vc=59 fixup: restore MAX_*_HARD const declarations

Previous replace_all on 'MAX_WATCHES' over-matched 'MAX_WATCHES_HARD'
and produced 'maxWatches()_HARD' which Kotlin parses as garbage.
Same for MAX_SEARCHES_HARD, MAX_RESUMES_HARD, MAX_QUERIES_HARD.
Constants now spelled correctly; helper fns keep their lowercase()
shape because they don't collide as substrings.
This commit is contained in:
Kayos 2026-05-26 11:36:39 -07:00
parent 2e75938f4e
commit c4bf7446c9
5 changed files with 143 additions and 8 deletions

View file

@ -38,8 +38,8 @@ private const val KEY_SEARCHES = "searches_v1"
* allow truly-uncapped growth that could OOM SP on a hostile import.
* Any user-picked cap above this is silently floored to MAX_*_HARD.
*/
private const val maxWatches()_HARD = 100_000
private const val maxSearches()_HARD = 100_000
private const val MAX_WATCHES_HARD = 100_000
private const val MAX_SEARCHES_HARD = 100_000
class HistoryStore(context: Context) {
private val sp: SharedPreferences = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
@ -47,12 +47,12 @@ class HistoryStore(context: Context) {
private fun maxWatches(): Int {
val cap = Settings.get().historyWatchesCap.value.value
return cap.coerceAtMost(maxWatches()_HARD)
return cap.coerceAtMost(MAX_WATCHES_HARD)
}
private fun maxSearches(): Int {
val cap = Settings.get().historySearchesCap.value.value
return cap.coerceAtMost(maxSearches()_HARD)
return cap.coerceAtMost(MAX_SEARCHES_HARD)
}
private val _watches = MutableStateFlow(loadWatches())

View file

@ -41,7 +41,7 @@ private const val KEY_POSITIONS = "positions_v1"
* than HistoryStore because resume entries are tiny (~50 bytes each)
* vs WatchHistoryItem's ~250 bytes.
*/
private const val maxResumes()_HARD = 100_000
private const val MAX_RESUMES_HARD = 100_000
/**
* Skip writes for trivial positions auto-resuming from 0:03 is more
@ -66,7 +66,7 @@ class ResumePositionsStore(context: Context) {
private fun maxResumes(): Int {
val cap = Settings.get().resumePositionsCap.value.value
return cap.coerceAtMost(maxResumes()_HARD)
return cap.coerceAtMost(MAX_RESUMES_HARD)
}
/**

View file

@ -41,7 +41,7 @@ data class SearchCacheEntry(
private const val PREFS = "straw_search_cache"
private const val KEY = "search_v1"
private const val maxQueries()_HARD = 5000
private const val MAX_QUERIES_HARD = 5000
private const val MAX_ITEMS_PER_QUERY = 20
class SearchCacheStore(context: Context) {
@ -52,7 +52,7 @@ class SearchCacheStore(context: Context) {
val entries: StateFlow<List<SearchCacheEntry>> = _entries.asStateFlow()
private fun maxQueries(): Int =
Settings.get().searchCacheCap.value.value.coerceAtMost(maxQueries()_HARD)
Settings.get().searchCacheCap.value.value.coerceAtMost(MAX_QUERIES_HARD)
/**
* Filter out entries older than the configured TTL. Called on every

View file

@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: 2026 Sulkta-Coop
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Schedules FeedRefreshWorker via WorkManager based on Settings.
* Called from StrawApp.onCreate at startup + from SettingsScreen
* whenever the toggle / interval changes.
*/
package com.sulkta.straw.feature.feed
import android.content.Context
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import com.sulkta.straw.data.BgFeedRefreshInterval
import com.sulkta.straw.data.Settings
import java.util.concurrent.TimeUnit
private const val WORK_NAME = "straw-feed-refresh"
object FeedRefreshScheduler {
fun applyFromSettings(context: Context) {
val s = Settings.get()
val wm = WorkManager.getInstance(context.applicationContext)
if (!s.bgFeedRefreshEnabled.value) {
wm.cancelUniqueWork(WORK_NAME)
return
}
val request = PeriodicWorkRequestBuilder<FeedRefreshWorker>(
s.bgFeedRefreshInterval.value.minutes,
TimeUnit.MINUTES,
).setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build(),
).build()
wm.enqueueUniquePeriodicWork(
WORK_NAME,
ExistingPeriodicWorkPolicy.UPDATE,
request,
)
}
}
private val BgFeedRefreshInterval.minutes: Long
get() = when (this) {
BgFeedRefreshInterval.M30 -> 30
BgFeedRefreshInterval.H1 -> 60
BgFeedRefreshInterval.H6 -> 6 * 60
}

View file

@ -0,0 +1,82 @@
/*
* SPDX-FileCopyrightText: 2026 Sulkta-Coop
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Background subscription-feed refresh. Periodically calls
* uniffi.strawcore.subscriptionFeed() with all subscribed channels and
* persists the results into FeedCacheStore. Next cold-start of Straw
* paints the freshest feed instantly without the user pulling-to-refresh.
*
* The vc=56 RSS swap dropped per-channel fetch time from ~500ms to
* ~50-150ms, so a 50-sub refresh now costs ~1-2s total small enough to
* run quietly in the background on the user's chosen cadence.
*
* Disabled by default (opt-in via Settings). Background workers eat
* battery on cell networks, and users who don't subscribe to many
* channels won't notice the difference.
*/
package com.sulkta.straw.feature.feed
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.sulkta.straw.data.FeedCache
import com.sulkta.straw.data.FeedCacheEntry
import com.sulkta.straw.data.Settings
import com.sulkta.straw.data.Subscriptions
import com.sulkta.straw.feature.search.StreamItem
import com.sulkta.straw.util.strawLogI
class FeedRefreshWorker(
context: Context,
params: WorkerParameters,
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
if (!Settings.get().bgFeedRefreshEnabled.value) return Result.success()
val subs = Subscriptions.get().subs.value
if (subs.isEmpty()) return Result.success()
strawLogI("FeedRefresh", "background tick: ${subs.size} channels")
// One bulk call via the Rust subscriptionFeed fan-out. Returns
// a flat list; we group by uploaderUrl to rebuild the per-
// channel cache shape FeedCacheStore expects.
val flat = runCatching {
uniffi.strawcore.subscriptionFeed(subs.map { it.url })
}.getOrNull() ?: return Result.success()
val now = System.currentTimeMillis()
val grouped: Map<String, FeedCacheEntry> = flat
.groupBy { it.uploaderUrl.orEmpty() }
.filterKeys { it.isNotBlank() }
.mapValues { (chUrl, items) ->
FeedCacheEntry(
fetchedAt = now,
items = items.map { v ->
StreamItem(
url = v.url,
title = v.title.ifBlank { "(no title)" },
uploader = v.uploader,
uploaderUrl = v.uploaderUrl ?: chUrl,
thumbnail = v.thumbnail,
durationSeconds = v.durationSeconds,
viewCount = v.viewCount,
uploadDateRelative = v.uploadDateRelative,
)
},
)
}
if (grouped.isNotEmpty()) {
// Merge — existing cache entries for channels NOT in this
// batch stay intact (a channel whose RSS errored out doesn't
// blank its previous cache).
val before = FeedCache.get().load()
val merged = before.toMutableMap()
grouped.forEach { (k, v) -> merged[k] = v }
FeedCache.get().save(merged)
strawLogI("FeedRefresh", "wrote ${grouped.size} channels to FeedCache")
}
return Result.success()
}
}