Skip to content

Commit a09c957

Browse files
authored
Merge pull request #265 from rainxchzed/caching-system
2 parents c744ce9 + a6e7c52 commit a09c957

31 files changed

Lines changed: 1038 additions & 148 deletions

File tree

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,15 @@ fun AppNavigation(
213213
ProfileRoot(
214214
onNavigateBack = {
215215
navController.navigateUp()
216+
},
217+
onNavigateToAuthentication = {
218+
navController.navigate(GithubStoreGraph.AuthenticationScreen)
219+
},
220+
onNavigateToStarredRepos = {
221+
navController.navigate(GithubStoreGraph.StarredReposScreen)
222+
},
223+
onNavigateToFavouriteRepos = {
224+
navController.navigate(GithubStoreGraph.FavouritesScreen)
216225
}
217226
)
218227
}

core/data/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ kotlin {
1919

2020
implementation(libs.datastore)
2121
implementation(libs.datastore.preferences)
22+
23+
implementation(libs.kotlinx.datetime)
2224
}
2325
}
2426

core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/initDatabase.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import androidx.room.Room
55
import kotlinx.coroutines.Dispatchers
66
import zed.rainxch.core.data.local.db.migrations.MIGRATION_1_2
77
import zed.rainxch.core.data.local.db.migrations.MIGRATION_2_3
8+
import zed.rainxch.core.data.local.db.migrations.MIGRATION_3_4
89

910
fun initDatabase(context: Context): AppDatabase {
1011
val appContext = context.applicationContext
@@ -18,6 +19,7 @@ fun initDatabase(context: Context): AppDatabase {
1819
.addMigrations(
1920
MIGRATION_1_2,
2021
MIGRATION_2_3,
22+
MIGRATION_3_4,
2123
)
2224
.build()
2325
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package zed.rainxch.core.data.local.db.migrations
2+
3+
import androidx.room.migration.Migration
4+
import androidx.sqlite.db.SupportSQLiteDatabase
5+
6+
val MIGRATION_3_4 = object : Migration(3, 4) {
7+
override fun migrate(db: SupportSQLiteDatabase) {
8+
db.execSQL("""
9+
CREATE TABLE IF NOT EXISTS cache_entries (
10+
`key` TEXT NOT NULL,
11+
jsonData TEXT NOT NULL,
12+
cachedAt INTEGER NOT NULL,
13+
expiresAt INTEGER NOT NULL,
14+
PRIMARY KEY(`key`)
15+
)
16+
""".trimIndent())
17+
}
18+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package zed.rainxch.core.data.cache
2+
3+
import kotlinx.serialization.json.Json
4+
import kotlinx.serialization.serializer
5+
import zed.rainxch.core.data.local.db.dao.CacheDao
6+
import zed.rainxch.core.data.local.db.entities.CacheEntryEntity
7+
import kotlin.time.Clock
8+
import kotlin.time.Duration.Companion.hours
9+
10+
class CacheManager(
11+
val cacheDao: CacheDao
12+
) {
13+
val json = Json {
14+
ignoreUnknownKeys = true
15+
isLenient = true
16+
encodeDefaults = true
17+
}
18+
19+
val memoryCache = HashMap<String, Pair<Long, String>>()
20+
21+
fun now(): Long = Clock.System.now().toEpochMilliseconds()
22+
23+
suspend inline fun <reified T> get(key: String): T? {
24+
val currentTime = now()
25+
26+
memoryCache[key]?.let { (expiresAt, jsonData) ->
27+
if (expiresAt > currentTime) {
28+
return try {
29+
json.decodeFromString(serializer<T>(), jsonData)
30+
} catch (_: Exception) {
31+
memoryCache.remove(key)
32+
null
33+
}
34+
} else {
35+
memoryCache.remove(key)
36+
}
37+
}
38+
39+
val entry = cacheDao.getValid(key, currentTime) ?: return null
40+
memoryCache[key] = entry.expiresAt to entry.jsonData
41+
42+
return try {
43+
json.decodeFromString(serializer<T>(), entry.jsonData)
44+
} catch (_: Exception) {
45+
cacheDao.delete(key)
46+
memoryCache.remove(key)
47+
null
48+
}
49+
}
50+
51+
suspend inline fun <reified T> getStale(key: String): T? {
52+
val entry = cacheDao.getAny(key) ?: return null
53+
return try {
54+
json.decodeFromString(serializer<T>(), entry.jsonData)
55+
} catch (_: Exception) {
56+
null
57+
}
58+
}
59+
60+
suspend inline fun <reified T> put(key: String, value: T, ttlMillis: Long) {
61+
val currentTime = now()
62+
val jsonData = json.encodeToString(serializer<T>(), value)
63+
val expiresAt = currentTime + ttlMillis
64+
65+
memoryCache[key] = expiresAt to jsonData
66+
67+
cacheDao.put(
68+
CacheEntryEntity(
69+
key = key,
70+
jsonData = jsonData,
71+
cachedAt = currentTime,
72+
expiresAt = expiresAt
73+
)
74+
)
75+
}
76+
77+
suspend fun invalidate(key: String) {
78+
memoryCache.remove(key)
79+
cacheDao.delete(key)
80+
}
81+
82+
suspend fun invalidateByPrefix(prefix: String) {
83+
val keysToRemove = memoryCache.keys.filter { it.startsWith(prefix) }
84+
keysToRemove.forEach { memoryCache.remove(it) }
85+
cacheDao.deleteByPrefix(prefix)
86+
}
87+
88+
suspend fun cleanupExpired() {
89+
val currentTime = now()
90+
val expiredKeys = memoryCache.entries
91+
.filter { it.value.first <= currentTime }
92+
.map { it.key }
93+
expiredKeys.forEach { memoryCache.remove(it) }
94+
cacheDao.deleteExpired(currentTime)
95+
}
96+
97+
companion object CacheTtl {
98+
val HOME_REPOS = 12.hours.inWholeMilliseconds
99+
val REPO_DETAILS = 6.hours.inWholeMilliseconds
100+
val RELEASES = 6.hours.inWholeMilliseconds
101+
val README = 12.hours.inWholeMilliseconds
102+
val USER_PROFILE = 6.hours.inWholeMilliseconds
103+
val SEARCH_RESULTS = 1.hours.inWholeMilliseconds
104+
val REPO_STATS = 6.hours.inWholeMilliseconds
105+
}
106+
}

core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import kotlinx.coroutines.flow.first
88
import kotlinx.coroutines.runBlocking
99
import kotlinx.coroutines.withTimeout
1010
import org.koin.dsl.module
11+
import zed.rainxch.core.data.cache.CacheManager
1112
import zed.rainxch.core.data.data_source.TokenStore
1213
import zed.rainxch.core.data.data_source.impl.DefaultTokenStore
1314
import zed.rainxch.core.data.local.db.AppDatabase
15+
import zed.rainxch.core.data.local.db.dao.CacheDao
1416
import zed.rainxch.core.data.local.db.dao.FavoriteRepoDao
1517
import zed.rainxch.core.data.local.db.dao.InstalledAppDao
1618
import zed.rainxch.core.data.local.db.dao.StarredRepoDao
@@ -104,6 +106,10 @@ val coreModule = module {
104106
logger = get()
105107
)
106108
}
109+
110+
single<CacheManager> {
111+
CacheManager(cacheDao = get())
112+
}
107113
}
108114

109115
val networkModule = module {
@@ -175,4 +181,8 @@ val databaseModule = module {
175181
single<UpdateHistoryDao> {
176182
get<AppDatabase>().updateHistoryDao
177183
}
184+
185+
single<CacheDao> {
186+
get<AppDatabase>().cacheDao
187+
}
178188
}

core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/AppDatabase.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package zed.rainxch.core.data.local.db
22

33
import androidx.room.Database
44
import androidx.room.RoomDatabase
5+
import zed.rainxch.core.data.local.db.dao.CacheDao
56
import zed.rainxch.core.data.local.db.dao.FavoriteRepoDao
67
import zed.rainxch.core.data.local.db.dao.InstalledAppDao
78
import zed.rainxch.core.data.local.db.dao.StarredRepoDao
89
import zed.rainxch.core.data.local.db.dao.UpdateHistoryDao
10+
import zed.rainxch.core.data.local.db.entities.CacheEntryEntity
911
import zed.rainxch.core.data.local.db.entities.FavoriteRepoEntity
1012
import zed.rainxch.core.data.local.db.entities.InstalledAppEntity
1113
import zed.rainxch.core.data.local.db.entities.StarredRepositoryEntity
@@ -17,13 +19,15 @@ import zed.rainxch.core.data.local.db.entities.UpdateHistoryEntity
1719
FavoriteRepoEntity::class,
1820
UpdateHistoryEntity::class,
1921
StarredRepositoryEntity::class,
22+
CacheEntryEntity::class,
2023
],
21-
version = 3,
24+
version = 4,
2225
exportSchema = true
2326
)
2427
abstract class AppDatabase : RoomDatabase() {
2528
abstract val installedAppDao: InstalledAppDao
2629
abstract val favoriteRepoDao: FavoriteRepoDao
2730
abstract val updateHistoryDao: UpdateHistoryDao
2831
abstract val starredReposDao: StarredRepoDao
29-
}
32+
abstract val cacheDao: CacheDao
33+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package zed.rainxch.core.data.local.db.dao
2+
3+
import androidx.room.Dao
4+
import androidx.room.Insert
5+
import androidx.room.OnConflictStrategy
6+
import androidx.room.Query
7+
import zed.rainxch.core.data.local.db.entities.CacheEntryEntity
8+
9+
@Dao
10+
interface CacheDao {
11+
@Query("SELECT * FROM cache_entries WHERE `key` = :key AND expiresAt > :now LIMIT 1")
12+
suspend fun getValid(key: String, now: Long): CacheEntryEntity?
13+
14+
@Query("SELECT * FROM cache_entries WHERE `key` = :key LIMIT 1")
15+
suspend fun getAny(key: String): CacheEntryEntity?
16+
17+
@Query("SELECT * FROM cache_entries WHERE `key` LIKE :prefix || '%' AND expiresAt > :now")
18+
suspend fun getValidByPrefix(prefix: String, now: Long): List<CacheEntryEntity>
19+
20+
@Insert(onConflict = OnConflictStrategy.REPLACE)
21+
suspend fun put(entry: CacheEntryEntity)
22+
23+
@Query("DELETE FROM cache_entries WHERE `key` = :key")
24+
suspend fun delete(key: String)
25+
26+
@Query("DELETE FROM cache_entries WHERE `key` LIKE :prefix || '%'")
27+
suspend fun deleteByPrefix(prefix: String)
28+
29+
@Query("DELETE FROM cache_entries WHERE expiresAt <= :now")
30+
suspend fun deleteExpired(now: Long)
31+
32+
@Query("DELETE FROM cache_entries")
33+
suspend fun deleteAll()
34+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package zed.rainxch.core.data.local.db.entities
2+
3+
import androidx.room.Entity
4+
import androidx.room.PrimaryKey
5+
6+
@Entity(tableName = "cache_entries")
7+
data class CacheEntryEntity(
8+
@PrimaryKey
9+
val key: String,
10+
val jsonData: String,
11+
val cachedAt: Long,
12+
val expiresAt: Long
13+
)

core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/GithubAsset.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package zed.rainxch.core.domain.model
22

3+
import kotlinx.serialization.Serializable
4+
5+
@Serializable
36
data class GithubAsset(
47
val id: Long,
58
val name: String,

0 commit comments

Comments
 (0)