From c74b06436f6795d2fee76952908dbd1c9650f185 Mon Sep 17 00:00:00 2001 From: Kayos Date: Mon, 25 May 2026 12:54:09 -0700 Subject: [PATCH] vc=33 fix: add FeedCacheStore.kt to git git commit -am only stages tracked files. The new file existed locally but wasn't in the previous commit; build failed with Unresolved reference 'FeedCache' on every site that used it. --- .../com/sulkta/straw/data/FeedCacheStore.kt | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 strawApp/src/main/kotlin/com/sulkta/straw/data/FeedCacheStore.kt diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/data/FeedCacheStore.kt b/strawApp/src/main/kotlin/com/sulkta/straw/data/FeedCacheStore.kt new file mode 100644 index 000000000..70d9a2c1b --- /dev/null +++ b/strawApp/src/main/kotlin/com/sulkta/straw/data/FeedCacheStore.kt @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2026 Sulkta-Coop + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Persistent per-channel cache for the subscription feed. Survives + * process death, so opening Subs after a cold start shows the last + * successful fetch immediately instead of waiting 5+ seconds for 30 + * channel browses to resolve. + * + * Storage: SharedPreferences with a single JSON blob. Total payload is + * small (30 subs * 30 items * ~250 bytes = ~225 KB), well within SP's + * comfortable size and well below the multi-MB threshold where you'd + * want to graduate to Room or a file. + * + * Concurrency: writes from the feed VM are debounced via the single + * `persist` call inside fetchChannelInto's success path. Reads happen + * on VM init and are synchronous. + */ + +package com.sulkta.straw.data + +import android.content.Context +import android.content.SharedPreferences +import com.sulkta.straw.feature.search.StreamItem +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +@Serializable +data class FeedCacheEntry( + val fetchedAt: Long, + val items: List, +) + +private const val PREFS = "straw_feed_cache" +private const val KEY = "cache_v1" + +class FeedCacheStore(context: Context) { + private val sp: SharedPreferences = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + private val json = Json { ignoreUnknownKeys = true; isLenient = true } + + /** Snapshot of the disk cache. Returns empty map if nothing saved. */ + fun load(): Map = runCatching { + val s = sp.getString(KEY, null) ?: return emptyMap() + json.decodeFromString>(s) + }.getOrDefault(emptyMap()) + + /** Atomic write. Caller is responsible for diffing if needed. */ + fun save(map: Map) { + val s = json.encodeToString(map) + sp.edit().putString(KEY, s).apply() + } + + fun clear() { + sp.edit().remove(KEY).apply() + } +} + +object FeedCache { + @Volatile private var instance: FeedCacheStore? = null + + fun init(context: Context) { + if (instance == null) { + synchronized(this) { + if (instance == null) instance = FeedCacheStore(context.applicationContext) + } + } + } + + fun get(): FeedCacheStore = instance + ?: error("FeedCacheStore not initialized — call FeedCache.init(context)") +}