Playlist-track atomic replace + ci-requirements true-up #115

Merged
bvandeusen merged 2 commits from dev into main 2026-08-01 12:23:38 -04:00
2 changed files with 28 additions and 4 deletions
Showing only changes of commit cf7b489fec - Show all commits
@@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction
import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistTrackEntity import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistTrackEntity
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -35,10 +36,33 @@ interface CachedPlaylistTrackDao {
@Query("SELECT MAX(position) FROM cached_playlist_tracks WHERE playlistId = :playlistId") @Query("SELECT MAX(position) FROM cached_playlist_tracks WHERE playlistId = :playlistId")
suspend fun maxPosition(playlistId: String): Int? suspend fun maxPosition(playlistId: String): Int?
/** Replace-all pattern for a playlist; called after a sync delta lands. */
@Query("DELETE FROM cached_playlist_tracks WHERE playlistId = :playlistId") @Query("DELETE FROM cached_playlist_tracks WHERE playlistId = :playlistId")
suspend fun deleteByPlaylist(playlistId: String) suspend fun deleteByPlaylist(playlistId: String)
/**
* Replaces a playlist's whole membership in ONE transaction; called
* after a refresh or a sync delta lands.
*
* Atomic on purpose. Room's InvalidationTracker only notifies observers
* after the transaction commits, so [observeByPlaylist] never sees the
* empty gap between the delete and the re-insert. Un-transacted, that
* gap is a real observed state — it's what made every Home row visibly
* collapse to empty and refill before issue #2327 fixed the equivalent
* write in `CachedHomeIndexDao`.
*
* Nothing observes [observeByPlaylist] live today, so this is
* pre-emptive: it means making playlist detail cache-first later can't
* silently reintroduce that flicker.
*/
@Transaction
suspend fun replacePlaylistTracks(
playlistId: String,
rows: List<CachedPlaylistTrackEntity>,
) {
deleteByPlaylist(playlistId)
if (rows.isNotEmpty()) upsertAll(rows)
}
@Query( @Query(
"DELETE FROM cached_playlist_tracks " + "DELETE FROM cached_playlist_tracks " +
"WHERE playlistId = :playlistId AND trackId IN (:trackIds)", "WHERE playlistId = :playlistId AND trackId IN (:trackIds)",
@@ -119,9 +119,9 @@ class PlaylistsRepository @Inject constructor(
throw e throw e
} }
playlistDao.upsertAll(listOf(wire.toPlaylistEntity())) playlistDao.upsertAll(listOf(wire.toPlaylistEntity()))
playlistTrackDao.deleteByPlaylist(id) playlistTrackDao.replacePlaylistTracks(
playlistTrackDao.upsertAll( playlistId = id,
wire.tracks.mapNotNull { row -> rows = wire.tracks.mapNotNull { row ->
row.trackId?.let { trackId -> row.trackId?.let { trackId ->
CachedPlaylistTrackEntity( CachedPlaylistTrackEntity(
playlistId = id, playlistId = id,