From 429594e6bc1069f7e7141060df3a0fd8bf63749b Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sat, 22 Aug 2026 01:06:35 +0000 Subject: Concurrent feed fetching with durable fetch-result tracking Feeds are fetched with bounded concurrency (semaphore of 8) instead of strictly sequentially -- with 100+ subscriptions, one slow/stalling server no longer delays every feed queued behind it. A process-local mutex prevents the periodic and manual/alarm-triggered one-time fetches (two different WorkManager unique-work names, so its own dedup doesn't cover this) from ever running concurrently and racing writes to the same feed rows. Settings.recordFetchResult persists lastFetchAt/lastFetchNewArticles on every run, not derived from WorkInfo.State -- a PeriodicWorkRequest cycles back to ENQUEUED after every successful run and never reaches SUCCEEDED, so anything watching for that only ever saw manual fetches. ArticleListScreen surfaces this as a "N new articles" snackbar (acknowledged durably, so returning to the screen doesn't re-show it) and computes live "Checking feed X of Y" progress from the worker's tagged WorkInfo, scoped to the Unread/Older views that actually aggregate every subscription. --- .../java/org/terst/feedreader/data/Settings.kt | 33 ++++++++++++++++++++++ 1 file changed, 33 insertions(+) (limited to 'app/src/main/java/org/terst/feedreader/data') diff --git a/app/src/main/java/org/terst/feedreader/data/Settings.kt b/app/src/main/java/org/terst/feedreader/data/Settings.kt index b4af70e..ce05974 100644 --- a/app/src/main/java/org/terst/feedreader/data/Settings.kt +++ b/app/src/main/java/org/terst/feedreader/data/Settings.kt @@ -2,6 +2,8 @@ package org.terst.feedreader.data import android.content.Context import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.Flow @@ -13,6 +15,9 @@ val Context.settingsDataStore by preferencesDataStore(name = "settings") private object SettingsKeys { val STASH_BASE_URL = stringPreferencesKey("stash_base_url") val STASH_API_KEY = stringPreferencesKey("stash_api_key") + val LAST_FETCH_AT = longPreferencesKey("last_fetch_at") + val LAST_FETCH_NEW_ARTICLES = intPreferencesKey("last_fetch_new_articles") + val LAST_ACKNOWLEDGED_FETCH_AT = longPreferencesKey("last_acknowledged_fetch_at") } /** Stash connection settings, configured on the Settings screen. Both blank until set. */ @@ -31,4 +36,32 @@ class Settings(private val context: Context) { prefs[SettingsKeys.STASH_API_KEY] = apiKey.trim() } } + + // Durable record of "when did a feed fetch last finish, and with how many new + // articles" -- deliberately NOT derived from WorkManager's WorkInfo.State. A + // PeriodicWorkRequest's state cycles back to ENQUEUED after every successful run + // (it never settles on SUCCEEDED the way a one-time request does), so anything + // watching for SUCCEEDED only ever sees manual/alarm-triggered one-time fetches -- + // the actual hourly background fetch is invisible to that approach. Writing the + // result here on every run (periodic or one-time alike) gives the UI one consistent + // signal regardless of which triggered it. + val lastFetchAt: Flow = + context.settingsDataStore.data.map { it[SettingsKeys.LAST_FETCH_AT] ?: 0L } + val lastFetchNewArticles: Flow = + context.settingsDataStore.data.map { it[SettingsKeys.LAST_FETCH_NEW_ARTICLES] ?: 0 } + val lastAcknowledgedFetchAt: Flow = + context.settingsDataStore.data.map { it[SettingsKeys.LAST_ACKNOWLEDGED_FETCH_AT] ?: 0L } + + suspend fun recordFetchResult(newArticles: Int) { + context.settingsDataStore.edit { prefs -> + prefs[SettingsKeys.LAST_FETCH_AT] = System.currentTimeMillis() + prefs[SettingsKeys.LAST_FETCH_NEW_ARTICLES] = newArticles + } + } + + // Persisted (not just in-memory Compose state) so that navigating away from a screen + // and back doesn't forget a fetch was already acknowledged and re-show its snackbar. + suspend fun acknowledgeFetch(at: Long) { + context.settingsDataStore.edit { prefs -> prefs[SettingsKeys.LAST_ACKNOWLEDGED_FETCH_AT] = at } + } } -- cgit v1.2.3