feat(android): add HomeArtistPrewarmer (top-8 artists, session-deduped)

This commit is contained in:
2026-05-28 18:58:29 -04:00
parent b2512fff4b
commit 4eb225914c
2 changed files with 58 additions and 0 deletions
@@ -0,0 +1,33 @@
package com.fabledsword.minstrel.metadata
import java.util.Collections
import javax.inject.Inject
import javax.inject.Singleton
private const val TOP_N = 8
/**
* Pre-warms the Room cache for likely-tap artist tiles on Home. Mirrors
* Flutter's `MetadataPrefetcher`: conservative — artists only (a single
* round-trip per id), top-8 per call, and once per id per session so
* repeated index refreshes don't re-fetch. Albums are intentionally not
* pre-warmed (their detail fetch fans out a track list).
*
* Fetches go through [MetadataProvider.refreshArtist], so they share the
* same per-id dedup AND the 4-slot concurrency gate as on-render
* hydration — the prewarm can't starve the tiles the user is looking at.
*/
@Singleton
class HomeArtistPrewarmer @Inject constructor(
private val metadataProvider: MetadataProvider,
) {
private val warmed = Collections.synchronizedSet(mutableSetOf<String>())
fun warm(artistIds: List<String>) {
artistIds.asSequence()
.filter { it.isNotEmpty() }
.filter { warmed.add(it) }
.take(TOP_N)
.forEach { metadataProvider.refreshArtist(it) }
}
}
@@ -0,0 +1,25 @@
package com.fabledsword.minstrel.metadata
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.Test
class HomeArtistPrewarmerTest {
private val provider = mockk<MetadataProvider>()
@Test
fun `warms at most the top eight distinct ids, once per session`() {
every { provider.refreshArtist(any()) } returns mockk(relaxed = true)
val prewarmer = HomeArtistPrewarmer(provider)
// 10 ids with a duplicate and a blank -> 8 distinct non-blank warmed.
prewarmer.warm(listOf("a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", ""))
// Re-warm: already-warmed ids are skipped.
prewarmer.warm(listOf("a1", "a2"))
(1..8).forEach { i -> verify(exactly = 1) { provider.refreshArtist("a$i") } }
verify(exactly = 0) { provider.refreshArtist("a9") } // past the top-8 cap
verify(exactly = 0) { provider.refreshArtist("") } // blank skipped
}
}