Last active
August 2, 2025 07:40
-
-
Save smarteist/bd69149f92709c60e6fd0b7d172f7b0b to your computer and use it in GitHub Desktop.
Repository caching mechanisms
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * A generic, dispatcher-agnostic class for managing data with local and remote sources. | |
| * | |
| * This store provides different caching strategies and returns data via two main methods: | |
| * - `getAsFlow()`: Returns a [Flow] for reactive streams of data. | |
| * - `get()`: Returns a single value via a `suspend` function. | |
| * | |
| * The caller is responsible for specifying the execution context (e.g., using | |
| * `.flowOn(Dispatchers.IO)`) when using the Flow-based API. | |
| * | |
| * @param T The type of data being managed. | |
| * @param cacheRemoteDataSource The remote data source (e.g., API). | |
| * @param cacheLocalDataSource The local data source (e.g., Database). | |
| */ | |
| class CacheStore<T>( | |
| private val cacheRemoteDataSource: CacheRemoteDataSource<T>, | |
| private val cacheLocalDataSource: CacheLocalDataSource<T>, | |
| ) { | |
| /** Defines possible caching strategies for reading data. */ | |
| enum class Strategy { | |
| /** | |
| * Fetches from remote, updates local cache, and returns the remote result. Does not read | |
| * from the local cache first. | |
| */ | |
| READ_THROUGH, | |
| /** | |
| * Reads from the local cache. If data exists, returns it. Otherwise, fetches from remote, | |
| * updates the cache, and returns the remote result. | |
| */ | |
| READ_BACK, | |
| /** | |
| * For `getAsFlow`: Emits local data, then fetches remote and emits fresh data. For `get`: | |
| * Returns local data, but only after a *blocking* remote refresh. | |
| */ | |
| READ_LAZILY, | |
| } | |
| /** A generic RemoteDataSource interface for fetching data from a remote source (API, etc.). */ | |
| interface CacheRemoteDataSource<T> { | |
| suspend fun fetch(params: Map<String, Any?> = emptyMap()): T? | |
| } | |
| /** A generic LocalDataSource interface for reading/writing data in a local cache (DB, etc.). */ | |
| interface CacheLocalDataSource<T> { | |
| suspend fun get(params: Map<String, Any?> = emptyMap()): T? | |
| suspend fun put(value: T, params: Map<String, Any?> = emptyMap()) | |
| } | |
| // ============================================================================================= | |
| // Public APIs | |
| // ============================================================================================= | |
| /** | |
| * Returns a [Flow] of data based on the specified [Strategy]. | |
| * | |
| * **Important:** This function does not specify a CoroutineDispatcher. The caller is | |
| * responsible for using `.flowOn()` to provide an appropriate dispatcher (e.g., | |
| * `Dispatchers.IO`). | |
| * | |
| * @param strategy The caching strategy to use. | |
| * @param params A map of dynamic parameters for the data retrieval. | |
| * @return A [Flow] that emits the data. | |
| */ | |
| fun getAsFlow(strategy: Strategy, params: Map<String, Any?> = emptyMap()): Flow<T?> { | |
| return when (strategy) { | |
| Strategy.READ_THROUGH -> readThroughFlow(params) | |
| Strategy.READ_BACK -> readBackFlow(params) | |
| Strategy.READ_LAZILY -> lazyReadFlow(params) | |
| } | |
| } | |
| /** | |
| * Returns a single value based on the specified [Strategy]. This is a suspend function for | |
| * one-shot data retrieval. | |
| * | |
| * @param strategy The caching strategy to use. | |
| * @param params A map of dynamic parameters for the data retrieval. | |
| * @return A single nullable value of type [T]. | |
| */ | |
| suspend fun get(strategy: Strategy, params: Map<String, Any?> = emptyMap()): T? { | |
| return when (strategy) { | |
| Strategy.READ_THROUGH -> performReadThrough(params) | |
| Strategy.READ_BACK -> performReadBack(params) | |
| // Works as single shot | |
| Strategy.READ_LAZILY -> lazyReadSingleShot(params) | |
| } | |
| } | |
| // ============================================================================================= | |
| // Flow Implementations | |
| // ============================================================================================= | |
| private fun lazyReadFlow(params: Map<String, Any?>): Flow<T?> = flow { | |
| // 1. Emit local data immediately | |
| val localData = cacheLocalDataSource.get(params) | |
| emit(localData) | |
| // 2. Fetch remote, update local, and emit the new data | |
| try { | |
| val remoteData = cacheRemoteDataSource.fetch(params) | |
| if (remoteData != null) { | |
| cacheLocalDataSource.put(remoteData, params) | |
| // Emit the fresh data from the source of truth (the cache) | |
| emit(cacheLocalDataSource.get(params)) | |
| } | |
| } catch (ex: Exception) { | |
| ex.printStackTrace() | |
| } | |
| } | |
| private fun readThroughFlow(params: Map<String, Any?>): Flow<T?> = flow { | |
| // Wrap around flow | |
| emit(performReadThrough(params)) | |
| } | |
| private fun readBackFlow(params: Map<String, Any?>): Flow<T?> = flow { | |
| // Wrap around flow | |
| emit(performReadBack(params)) | |
| } | |
| // ============================================================================================= | |
| // Suspend Function Implementations | |
| // ============================================================================================= | |
| /** | |
| * Implements the [Strategy.READ_LAZILY] for the non-flow `get` method. It returns the original | |
| * local data, but only *after* waiting for the remote refresh to finish. | |
| */ | |
| private suspend fun lazyReadSingleShot(params: Map<String, Any?>): T? { | |
| val localData = cacheLocalDataSource.get(params) | |
| // This call is blocking; it will complete before the function returns. | |
| refreshCache(params) | |
| return localData | |
| } | |
| /** Core logic for the [Strategy.READ_THROUGH] strategy. */ | |
| private suspend fun performReadThrough(params: Map<String, Any?>): T? { | |
| return try { | |
| val remoteData = cacheRemoteDataSource.fetch(params) | |
| remoteData?.let { cacheLocalDataSource.put(it, params) } | |
| remoteData | |
| } catch (ex: Exception) { | |
| ex.printStackTrace() | |
| null | |
| } | |
| } | |
| /** Core logic for the [Strategy.READ_BACK] strategy. */ | |
| private suspend fun performReadBack(params: Map<String, Any?>): T? { | |
| val localData = cacheLocalDataSource.get(params) | |
| // If local data exists, return it. Otherwise, perform a read-through. | |
| return localData ?: performReadThrough(params) | |
| } | |
| /** Fetches from remote and updates the local cache. Used by [lazyReadSingleShot]. */ | |
| private suspend fun refreshCache(params: Map<String, Any?>) { | |
| try { | |
| val remoteData = cacheRemoteDataSource.fetch(params) | |
| remoteData?.let { cacheLocalDataSource.put(it, params) } | |
| } catch (ex: Exception) { | |
| ex.printStackTrace() | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment