@@ -2,154 +2,66 @@ package dev.tricked.solidverdant.data.local
22
33import android.content.Context
44import androidx.datastore.preferences.core.edit
5- import androidx.datastore.preferences.core.longPreferencesKey
65import androidx.datastore.preferences.core.stringPreferencesKey
76import androidx.datastore.preferences.preferencesDataStore
87import dagger.hilt.android.qualifiers.ApplicationContext
9- import dev.tricked.solidverdant.data.model.Project
10- import dev.tricked.solidverdant.data.model.Task
8+ import dev.tricked.solidverdant.data.model.*
119import kotlinx.coroutines.flow.first
10+ import kotlinx.serialization.Serializable
1211import kotlinx.serialization.json.Json
1312import timber.log.Timber
1413import javax.inject.Inject
1514import javax.inject.Singleton
1615
1716private val Context .cacheDataStore by preferencesDataStore(name = " cache_prefs" )
1817
19- /* *
20- * Simple cache using DataStore for projects and tasks
21- * Stores JSON serialized data with timestamps
22- */
18+ @Serializable
19+ data class ScreenSnapshot (
20+ val version : Int = CURRENT_VERSION ,
21+ val organizationId : String ,
22+ val user : User ,
23+ val memberships : List <Membership >,
24+ val currentMembershipId : String ,
25+ val timeEntries : List <TimeEntry >,
26+ val activeEntry : TimeEntry ? ,
27+ val projects : List <Project >,
28+ val tasks : List <Task >,
29+ val tags : List <Tag >,
30+ val savedAtEpochMs : Long = System .currentTimeMillis()
31+ ) {
32+ companion object { const val CURRENT_VERSION = 1 }
33+ }
34+
35+ /* * Stale-while-revalidate cache for the complete first tracking frame. */
2336@Singleton
2437class CacheDataStore @Inject constructor(
2538 @ApplicationContext private val context : Context ,
2639 private val json : Json
2740) {
28- companion object {
29- private val PROJECTS_JSON = stringPreferencesKey(" projects_json" )
30- private val PROJECTS_TIMESTAMP = longPreferencesKey(" projects_timestamp" )
31- private val TASKS_JSON = stringPreferencesKey(" tasks_json" )
32- private val TASKS_TIMESTAMP = longPreferencesKey(" tasks_timestamp" )
33-
34- // Cache TTL: 5 minutes
35- const val CACHE_TTL_MS = 5 * 60 * 1000L
36- }
37-
38- /* *
39- * Cache projects to DataStore
40- */
41- suspend fun cacheProjects (projects : List <Project >) {
42- try {
43- val jsonString = json.encodeToString(projects)
44- context.cacheDataStore.edit { prefs ->
45- prefs[PROJECTS_JSON ] = jsonString
46- prefs[PROJECTS_TIMESTAMP ] = System .currentTimeMillis()
47- }
48- Timber .d(" Cached ${projects.size} projects" )
49- } catch (e: Exception ) {
50- Timber .e(e, " Failed to cache projects" )
51- }
52- }
53-
54- /* *
55- * Get cached projects if not expired
56- * Returns null if cache is expired or doesn't exist
57- */
58- suspend fun getCachedProjects (): List <Project >? {
59- return try {
60- val prefs = context.cacheDataStore.data.first()
61- val timestamp = prefs[PROJECTS_TIMESTAMP ] ? : return null
62- val jsonString = prefs[PROJECTS_JSON ] ? : return null
63-
64- // Check if cache is expired
65- if (System .currentTimeMillis() - timestamp > CACHE_TTL_MS ) {
66- Timber .d(" Projects cache expired" )
67- return null
68- }
69-
70- val projects = json.decodeFromString<List <Project >>(jsonString)
71- Timber .d(" Retrieved ${projects.size} cached projects" )
72- projects
73- } catch (e: Exception ) {
74- Timber .e(e, " Failed to retrieve cached projects" )
75- null
76- }
41+ private companion object { val SNAPSHOT = stringPreferencesKey(" screen_snapshot_v1" ) }
42+
43+ suspend fun getScreenSnapshot (): ScreenSnapshot ? = try {
44+ val encoded = context.cacheDataStore.data.first()[SNAPSHOT ] ? : return null
45+ json.decodeFromString<ScreenSnapshot >(encoded).takeIf {
46+ it.version == ScreenSnapshot .CURRENT_VERSION
47+ }.also { if (it == null ) Timber .w(" Discarding old screen snapshot" ) }
48+ } catch (error: Exception ) {
49+ Timber .w(error, " Discarding corrupt screen snapshot" )
50+ clearCache()
51+ null
7752 }
7853
79- /* *
80- * Cache tasks to DataStore
81- */
82- suspend fun cacheTasks (tasks : List <Task >) {
54+ suspend fun saveScreenSnapshot (snapshot : ScreenSnapshot ) {
8355 try {
84- val jsonString = json.encodeToString(tasks)
85- context.cacheDataStore.edit { prefs ->
86- prefs[TASKS_JSON ] = jsonString
87- prefs[TASKS_TIMESTAMP ] = System .currentTimeMillis()
88- }
89- Timber .d(" Cached ${tasks.size} tasks" )
90- } catch (e: Exception ) {
91- Timber .e(e, " Failed to cache tasks" )
56+ val value = snapshot.copy(
57+ version = ScreenSnapshot .CURRENT_VERSION ,
58+ savedAtEpochMs = System .currentTimeMillis()
59+ )
60+ context.cacheDataStore.edit { it[SNAPSHOT ] = json.encodeToString(value) }
61+ } catch (error: Exception ) {
62+ Timber .e(error, " Failed to save screen snapshot" )
9263 }
9364 }
9465
95- /* *
96- * Get cached tasks if not expired
97- * Returns null if cache is expired or doesn't exist
98- */
99- suspend fun getCachedTasks (): List <Task >? {
100- return try {
101- val prefs = context.cacheDataStore.data.first()
102- val timestamp = prefs[TASKS_TIMESTAMP ] ? : return null
103- val jsonString = prefs[TASKS_JSON ] ? : return null
104-
105- // Check if cache is expired
106- if (System .currentTimeMillis() - timestamp > CACHE_TTL_MS ) {
107- Timber .d(" Tasks cache expired" )
108- return null
109- }
110-
111- val tasks = json.decodeFromString<List <Task >>(jsonString)
112- Timber .d(" Retrieved ${tasks.size} cached tasks" )
113- tasks
114- } catch (e: Exception ) {
115- Timber .e(e, " Failed to retrieve cached tasks" )
116- null
117- }
118- }
119-
120- /* *
121- * Check if cache is still valid (not expired)
122- */
123- suspend fun isCacheValid (): Boolean {
124- return try {
125- val prefs = context.cacheDataStore.data.first()
126- val projectsTimestamp = prefs[PROJECTS_TIMESTAMP ] ? : return false
127- val tasksTimestamp = prefs[TASKS_TIMESTAMP ] ? : return false
128-
129- val now = System .currentTimeMillis()
130- val projectsValid = (now - projectsTimestamp) <= CACHE_TTL_MS
131- val tasksValid = (now - tasksTimestamp) <= CACHE_TTL_MS
132-
133- projectsValid && tasksValid
134- } catch (e: Exception ) {
135- false
136- }
137- }
138-
139- /* *
140- * Clear all cached data
141- */
142- suspend fun clearCache () {
143- try {
144- context.cacheDataStore.edit { prefs ->
145- prefs.remove(PROJECTS_JSON )
146- prefs.remove(PROJECTS_TIMESTAMP )
147- prefs.remove(TASKS_JSON )
148- prefs.remove(TASKS_TIMESTAMP )
149- }
150- Timber .d(" Cache cleared" )
151- } catch (e: Exception ) {
152- Timber .e(e, " Failed to clear cache" )
153- }
154- }
66+ suspend fun clearCache () = context.cacheDataStore.edit { it.clear() }
15567}
0 commit comments