Skip to content

Commit

Permalink
caching logic
Browse files Browse the repository at this point in the history
  • Loading branch information
kotlitecture committed Jul 7, 2024
1 parent dc1584f commit ae7a26c
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<K>)` - 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<T>, value: T)` - Associates the specified value with the specified key in the cache.
- `clear()` - Clears all entries from the cache.

## Example
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,53 +9,53 @@ import kotlinx.coroutines.flow.update
*
* @param T The type of the cached value.
*/
interface CacheEntry<T> {
interface CacheEntry<T, K : CacheKey<T>> {

/** The key associated with this cache entry. */
val key: CacheKey<T>
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.
* The flow updates an value in the cache based on the expiration of the key.
*
* @return A flow representing the changes to the cached value.
*/
suspend fun getChanges(): Flow<T?>
suspend fun changes(): Flow<T?>

companion object {
/**
Expand All @@ -65,16 +65,17 @@ interface CacheEntry<T> {
* @param value The cached value.
* @return A CacheState instance representing the single cached value.
*/
fun <T> of(key: CacheKey<T>, value: T): CacheEntry<T> = object : CacheEntry<T> {
private val valueChanges = MutableStateFlow<T?>(value)
fun <T, K : CacheKey<T>> of(key: K, value: T): CacheEntry<T, K> =
object : CacheEntry<T, K> {
private val valueChanges = MutableStateFlow<T?>(value)

override val key: CacheKey<T> = key
override suspend fun getValue(): T? = value
override suspend fun getLast(): T? = value
override suspend fun getFresh(): T? = value
override suspend fun getChanges(): Flow<T?> = 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<T?> = valueChanges
override suspend fun set(value: T?) = valueChanges.update { value }
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T> get(key: CacheKey<T>, valueProvider: suspend () -> T?): CacheEntry<T>
fun <T, K : CacheKey<T>> get(key: K, valueProvider: suspend (key: K) -> T?): CacheEntry<T, K>

/**
* Invalidates all cache entries associated with the specified key type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,16 @@ open class InMemoryCacheSource(

private val dispatcher = Dispatchers.Default
private val jobs = ConcurrentMutableMap<KeyData<*>, Deferred<*>>()
private val cache = ConcurrentMutableMap<KeyData<*>, EntryData<*>>()
private val cache = ConcurrentMutableMap<KeyData<*>, EntryData<*, *>>()

override fun <T> get(key: CacheKey<T>, valueProvider: suspend () -> T?): CacheEntry<T> {
override fun <T, K : CacheKey<T>> get(
key: K,
valueProvider: suspend (key: K) -> T?
): CacheEntry<T, K> {
val keyData = KeyData(key)
val entryData = cache.computeIfAbsent(keyData) {
EntryData(keyData, valueProvider)
} as CacheEntry<T>
} as CacheEntry<T, K>
return entryData
}

Expand Down Expand Up @@ -108,8 +111,8 @@ open class InMemoryCacheSource(
cache.remove(cacheKey)
}

private data class KeyData<T>(
val key: CacheKey<T>,
private data class KeyData<K : CacheKey<*>>(
val key: K,
val type: KClass<*> = key::class
)

Expand All @@ -118,25 +121,25 @@ open class InMemoryCacheSource(
val updateTime: Long = Clock.System.now().toEpochMilliseconds()
)

private inner class EntryData<T>(
private val keyData: KeyData<T>,
private val valueProvider: suspend () -> T?
) : CacheEntry<T> {
private inner class EntryData<T, K : CacheKey<T>>(
private val keyData: KeyData<K>,
private val valueProvider: suspend (key: K) -> T?
) : CacheEntry<T, K> {

@Transient
private var invalidated = false
private val liveChanges by lazy { fetchLiveChanges() }
private val changes = MutableStateFlow<EntrySnapshot<T>?>(null)

override val key: CacheKey<T> = 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
Expand All @@ -145,7 +148,7 @@ open class InMemoryCacheSource(
}
}

override suspend fun getChanges(): Flow<T?> = liveChanges
override suspend fun changes(): Flow<T?> = liveChanges
.flatMapLatest { changes }
.map { snapshot -> snapshot?.value }
.retry { th -> !th.isCancellationException().also { delay(changesRetryInterval) } }
Expand Down Expand Up @@ -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) }
}
}
}
Expand All @@ -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) {
Expand All @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class InMemoryCacheSourceTest {
delay(2.seconds)
iteration
}
.getValue()
.get()
?.let(cached::add)
}
}
Expand All @@ -50,7 +50,7 @@ class InMemoryCacheSourceTest {
delay(1.seconds)
UUID.randomUUID()
}
.getValue()
.get()
?.let(cached::add)
}
}
Expand All @@ -71,7 +71,7 @@ class InMemoryCacheSourceTest {
delay(1.seconds)
UUID.randomUUID()
}
.getChanges()
.changes()
.filterNotNull()
.take(1)
.first()
Expand All @@ -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(
Expand Down

0 comments on commit ae7a26c

Please sign in to comment.