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:
parent
65265804f3
commit
d98d8cb0d4
5 changed files with 143 additions and 8 deletions
|
|
@ -38,8 +38,8 @@ private const val KEY_SEARCHES = "searches_v1"
|
||||||
* allow truly-uncapped growth that could OOM SP on a hostile import.
|
* allow truly-uncapped growth that could OOM SP on a hostile import.
|
||||||
* Any user-picked cap above this is silently floored to MAX_*_HARD.
|
* Any user-picked cap above this is silently floored to MAX_*_HARD.
|
||||||
*/
|
*/
|
||||||
private const val maxWatches()_HARD = 100_000
|
private const val MAX_WATCHES_HARD = 100_000
|
||||||
private const val maxSearches()_HARD = 100_000
|
private const val MAX_SEARCHES_HARD = 100_000
|
||||||
|
|
||||||
class HistoryStore(context: Context) {
|
class HistoryStore(context: Context) {
|
||||||
private val sp: SharedPreferences = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
private val sp: SharedPreferences = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
|
@ -47,12 +47,12 @@ class HistoryStore(context: Context) {
|
||||||
|
|
||||||
private fun maxWatches(): Int {
|
private fun maxWatches(): Int {
|
||||||
val cap = Settings.get().historyWatchesCap.value.value
|
val cap = Settings.get().historyWatchesCap.value.value
|
||||||
return cap.coerceAtMost(maxWatches()_HARD)
|
return cap.coerceAtMost(MAX_WATCHES_HARD)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun maxSearches(): Int {
|
private fun maxSearches(): Int {
|
||||||
val cap = Settings.get().historySearchesCap.value.value
|
val cap = Settings.get().historySearchesCap.value.value
|
||||||
return cap.coerceAtMost(maxSearches()_HARD)
|
return cap.coerceAtMost(MAX_SEARCHES_HARD)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _watches = MutableStateFlow(loadWatches())
|
private val _watches = MutableStateFlow(loadWatches())
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ private const val KEY_POSITIONS = "positions_v1"
|
||||||
* than HistoryStore because resume entries are tiny (~50 bytes each)
|
* than HistoryStore because resume entries are tiny (~50 bytes each)
|
||||||
* vs WatchHistoryItem's ~250 bytes.
|
* 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
|
* Skip writes for trivial positions — auto-resuming from 0:03 is more
|
||||||
|
|
@ -66,7 +66,7 @@ class ResumePositionsStore(context: Context) {
|
||||||
|
|
||||||
private fun maxResumes(): Int {
|
private fun maxResumes(): Int {
|
||||||
val cap = Settings.get().resumePositionsCap.value.value
|
val cap = Settings.get().resumePositionsCap.value.value
|
||||||
return cap.coerceAtMost(maxResumes()_HARD)
|
return cap.coerceAtMost(MAX_RESUMES_HARD)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ data class SearchCacheEntry(
|
||||||
|
|
||||||
private const val PREFS = "straw_search_cache"
|
private const val PREFS = "straw_search_cache"
|
||||||
private const val KEY = "search_v1"
|
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
|
private const val MAX_ITEMS_PER_QUERY = 20
|
||||||
|
|
||||||
class SearchCacheStore(context: Context) {
|
class SearchCacheStore(context: Context) {
|
||||||
|
|
@ -52,7 +52,7 @@ class SearchCacheStore(context: Context) {
|
||||||
val entries: StateFlow<List<SearchCacheEntry>> = _entries.asStateFlow()
|
val entries: StateFlow<List<SearchCacheEntry>> = _entries.asStateFlow()
|
||||||
|
|
||||||
private fun maxQueries(): Int =
|
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
|
* Filter out entries older than the configured TTL. Called on every
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Sulkta
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Sulkta
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue