Skip to content

Commit

Permalink
in memory cache source
Browse files Browse the repository at this point in the history
  • Loading branch information
kotlitecture committed Jun 23, 2024
1 parent 65824f5 commit 4be3516
Show file tree
Hide file tree
Showing 8 changed files with 504 additions and 0 deletions.
2 changes: 2 additions & 0 deletions template/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
Expand Down Expand Up @@ -76,6 +77,7 @@ sqldelight-sqlite-driver = { module = "app.cash.sqldelight:sqlite-driver", versi
sqldelight-web-worker-driver = { module = "app.cash.sqldelight:web-worker-driver", version.ref = "sqldelight" }
touchlab-kermit = { module = "co.touchlab:kermit", version.ref = "touchlab-kermit" }
touchlab-stately-common = { module = "co.touchlab:stately-common", version.ref = "touchlab-stately" }
touchlab-stately-concurrent-collections = { module = "co.touchlab:stately-concurrent-collections", version.ref = "touchlab-stately" }
touchlab-stately-isolate = { module = "co.touchlab:stately-isolate", version.ref = "touchlab-stately" }
touchlab-stately-iso-collections = { module = "co.touchlab:stately-iso-collections", version.ref = "touchlab-stately" }
touchlab-stately-iso-collections-js = { module = "co.touchlab:stately-iso-collections-js", version.ref = "touchlab-stately" }
Expand Down
5 changes: 5 additions & 0 deletions template/shared/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,15 @@ kotlin {
}
}
commonMain.dependencies {
api(libs.kotlinx.datetime)
api(libs.bundles.ktor.common)
api(libs.kotlinx.coroutines.core)
api(libs.kotlinx.serialization.json)
implementation(libs.multiplatform.settings.no.arg)
implementation(libs.touchlab.stately.concurrent.collections)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
// {platform.android.dependencies}
androidMain.dependencies {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package shared.data.datasource.cache

/**
* Interface representing a cache key for storing and retrieving data in the cache.
*
* @param T The type of the cached data.
*/
interface CacheKey<T> {

/**
* Time-to-live (TTL) for the cached data, in milliseconds.
*/
val ttl: Long

/**
* Determines if the cached data associated with this key is considered immortal.
* In terms of the API, immortality means that the cache entry initialization request
* is not bound to the lifecycle of the component from which it is called.
*
* For example, if a cache entry initialization request is called from a View (Fragment/Activity),
* it will be canceled once the view is destroyed.
* When a key is immortal, the cache entry will be initialized regardless of the component lifecycle.
*
* This can be useful when you need to proceed with some request without interruptions.
* For example, an OAuth refresh token actualization response needs to be completed and stored locally
* as an atomic action, so any further calls under OAuth authorization can continue with the newly obtained token.
* If such an action is processed on the server but interrupted and not stored on the client,
* it is possible that the old token becomes outdated and any further request to update it will fail.
*
* The immortal key helps reduce such issues by ensuring that cache entries are initialized
* even if the associated component lifecycle ends.
*
* @return {@code true} if the data associated with this key is immortal, {@code false} otherwise.
*/
fun immortal(): Boolean = false

companion object {
const val TTL_UNLIMITED = -1L
const val TTL_1_SECOND = 1_000L
const val TTL_3_SECONDS = 3_000L
const val TTL_5_SECONDS = 5_000L
const val TTL_10_SECONDS = 10_000L
const val TTL_15_SECONDS = 15_000L
const val TTL_30_SECONDS = 30_000L
const val TTL_60_SECONDS = 60_000L
const val TTL_5_MINUTES = 5 * 60_000L

/**
* Creates a new CacheKey with the specified time-to-live (TTL) duration.
*
* @param duration The time-to-live (TTL) duration for the cache key.
* @param immortal Specifies whether the cache key is immortal or not. Defaults to false.
* When set to true, the cache entry will not be bound to the lifecycle of the component
* from which it is initialized.
* @return A new CacheKey instance with the specified TTL duration and immortality status.
*/
fun <T> of(duration: Long, immortal: Boolean = false): CacheKey<T> = object : CacheKey<T> {
override val ttl: Long = duration
override fun immortal(): Boolean = immortal
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package shared.data.datasource.cache

import shared.data.datasource.DataSource
import kotlin.reflect.KClass

/**
* Provides an interface for a basic thread-safe caching mechanism, serving as an L1 Cache.
*
* The cache allows for storing and retrieving any in-memory data efficiently.
*
* It supports operations such as getting, putting, removing, and invalidating cache entries.
*/
interface CacheSource : DataSource {

/**
* Retrieves the state of a cache entry associated with the specified key.
* If the entry is not found in the cache, the provided value provider function is invoked to obtain the value.
*
* @param key The cache key associated with the entry.
* @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> getState(key: CacheKey<T>, valueProvider: suspend () -> T?): CacheState<T>

/**
* Retrieves the value associated with the specified key from the cache.
* If the value is not found in the cache, the provided value provider function is invoked to obtain the value.
*
* @param key The cache key associated with the value.
* @param valueProvider A suspend function that provides the value if it is not found in the cache.
* @return The value associated with the key, or null if not found.
*/
suspend fun <T> get(key: CacheKey<T>, valueProvider: suspend () -> T?): T?

/**
* Invalidates all cache entries associated with the specified key type.
*
* @param type The type of cache keys to invalidate.
*/
fun <K : CacheKey<*>> invalidate(type: KClass<K>)

/**
* Invalidates the cache entry associated with the specified key.
*
* @param key The cache key to invalidate.
*/
fun <K : CacheKey<*>> invalidate(key: K)

/**
* Removes all cache entries associated with the specified key type.
*
* @param type The type of cache keys to remove.
*/
fun <K : CacheKey<*>> remove(type: KClass<K>)

/**
* Removes the cache entry associated with the specified key.
*
* @param key The cache key to remove.
*/
fun <K : CacheKey<*>> remove(key: K)

/**
* Associates the specified value with the specified key in the cache.
*
*@param key The cache key to associate with the value.
* @param value The value to be stored in the cache.
*/
fun <T> put(key: CacheKey<T>, value: T)

/**
* Clears all entries from the cache.
*/
fun clear()

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package shared.data.datasource.cache

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf

/**
* Represents the state of a cache entry.
*
* @param T The type of the cached item.
*/
interface CacheState<T> {

/** The key associated with this cache state. */
val key: CacheKey<T>

/**
* Retrieves the cached item.
*
* @return The cached item, or new one if the item is not present in the cache or expired.
*/
suspend fun get(): T?

/**
* Retrieves the last cached item.
*
* @return The last cached item, or null if the item is not present in the cache.
*/
suspend fun last(): T?

/**
* Retrieves a fresh copy of the cached item.
*
* @return A fresh copy of the cached item, or null if the item is not available.
*/
suspend fun fresh(): T?

/**
* Retrieves the last cached item if available, otherwise retrieves a fresh copy of the item.
*
* @return The last cached item if available, or a fresh copy of the item. Returns null if the item is not present in the cache.
*/
suspend fun lastOrFresh() = last() ?: fresh()

/**
* Emits the cached item whenever it changes.
* The flow updates an item in the cache based on the expiration of the key.
*
* @return A flow representing the changes to the cached item.
*/
suspend fun changes(): Flow<T>

companion object {
/**
* Creates a CacheState instance representing a passed item.
*
* @param key The cache key associated with the item.
* @param item The cached item.
* @return A CacheState instance representing the single cached item.
*/
fun <T> single(key: CacheKey<T>, item: T): CacheState<T> = object : CacheState<T> {
override val key: CacheKey<T> = key
override suspend fun get(): T? = item
override suspend fun last(): T? = item
override suspend fun fresh(): T? = item
override suspend fun changes(): Flow<T> = flowOf(item)
}
}

}
Loading

0 comments on commit 4be3516

Please sign in to comment.