From c17e9573b77b4d404b11aeae0615febe88d5a84a Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:43:03 +0600 Subject: [PATCH] Reapply "Fix cache invalidation isn't done at startup (#2970)" This reverts commit d219c5e3bbcfb24c40fa69e40bff11b6fd81fd7f. # Conflicts: # app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt --- .../settings/screen/SettingsAdvancedScreen.kt | 7 ++- .../tachiyomi/data/download/DownloadCache.kt | 50 ++++++++++++++----- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt index b2e404e16..b57867e9c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt @@ -223,6 +223,7 @@ object SettingsAdvancedScreen : SearchableSettings { private fun getDataGroup(): Preference.PreferenceGroup { val context = LocalContext.current val navigator = LocalNavigator.currentOrThrow + val scope = rememberCoroutineScope() return Preference.PreferenceGroup( title = stringResource(MR.strings.label_data), @@ -231,8 +232,10 @@ object SettingsAdvancedScreen : SearchableSettings { title = stringResource(MR.strings.pref_invalidate_download_cache), subtitle = stringResource(MR.strings.pref_invalidate_download_cache_summary), onClick = { - Injekt.get().invalidateCache() - context.toast(MR.strings.download_cache_invalidated) + scope.launch { + Injekt.get().invalidateCache() + context.toast(MR.strings.download_cache_invalidated) + } }, ), Preference.PreferenceItem.TextPreference( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt index abfdf1a79..7cbb1aef4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt @@ -12,14 +12,17 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.receiveAsFlow @@ -109,13 +112,19 @@ class DownloadCache( ProtoBuf.decodeFromByteArray(it.readBytes()) } rootDownloadsDir = diskCache - lastRenew = System.currentTimeMillis() } } catch (e: Throwable) { logcat(LogPriority.ERROR, e) { "Failed to initialize from disk cache" } diskCacheFile.delete() } } + + sourceManager.catalogueSources + .map { sources -> sources.map { it.id }.toSet() } + .distinctUntilChanged() + .collect { + restartRenewal() + } } storageManager.changes @@ -353,19 +362,34 @@ class DownloadCache( notifyChanges() } - fun invalidateCache() { - lastRenew = 0L - renewalJob?.cancel() + suspend fun invalidateCache() { + renewalJob?.cancelAndJoin() diskCacheFile.delete() - renewCache() + lastRenew = 0L + renewCache(forceRenew = true) + } + + /** + * Safely cancels any in-progress renewal job, resets the last-renew timestamp, and + * immediately starts a new renewal, bypassing the time-based throttle. + */ + private fun restartRenewal() { + renewalJob?.cancel() + lastRenew = 0L + renewCache(forceRenew = true) } /** * Renews the downloads cache. + * + * @param forceRenew when `true`, the time-based throttle is bypassed. Use this after + * explicitly cancelling the previous job to avoid a race where the cancelled job's + * [invokeOnCompletion] handler sets [lastRenew] after the reset but before the new + * job's guard check. */ - private fun renewCache() { + private fun renewCache(forceRenew: Boolean = false) { // Avoid renewing cache if in the process nor too often - if (lastRenew + renewInterval >= System.currentTimeMillis() || renewalJob?.isActive == true) { + if ((!forceRenew && lastRenew + renewInterval >= System.currentTimeMillis()) || renewalJob?.isActive == true) { return } @@ -376,15 +400,14 @@ class DownloadCache( // Try to wait until extensions and sources have loaded // SY --> - var sources = emptyList() withTimeoutOrNull(30.seconds) { - extensionManager.isInitialized.first { it } - sourceManager.isInitialized.first { it } - - sources = getSources() + // SY <-- + sourceManager.catalogueSources.first { it.isNotEmpty() } + // SY --> } // SY <-- + val sources = getSources() val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id } rootDownloadsDirMutex.withLock { @@ -459,8 +482,9 @@ class DownloadCache( private var updateDiskCacheJob: Job? = null private fun updateDiskCache() { - updateDiskCacheJob?.cancel() + val previousJob = updateDiskCacheJob updateDiskCacheJob = scope.launchIO { + previousJob?.cancelAndJoin() delay(1000) ensureActive() val bytes = ProtoBuf.encodeToByteArray(rootDownloadsDir)