diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/AppDatabase.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/AppDatabase.kt index cb525e30..f9b5a1b0 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/AppDatabase.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/AppDatabase.kt @@ -7,6 +7,7 @@ import com.fabledsword.minstrel.cache.db.dao.AudioCacheIndexDao import com.fabledsword.minstrel.cache.db.dao.CachedAlbumDao import com.fabledsword.minstrel.cache.db.dao.CachedArtistDao import com.fabledsword.minstrel.cache.db.dao.CachedLikeDao +import com.fabledsword.minstrel.cache.db.dao.CachedMutationDao import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistDao import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistTrackDao import com.fabledsword.minstrel.cache.db.dao.CachedQuarantineDao @@ -16,6 +17,7 @@ import com.fabledsword.minstrel.cache.db.entities.AudioCacheIndexEntity import com.fabledsword.minstrel.cache.db.entities.CachedAlbumEntity import com.fabledsword.minstrel.cache.db.entities.CachedArtistEntity import com.fabledsword.minstrel.cache.db.entities.CachedLikeEntity +import com.fabledsword.minstrel.cache.db.entities.CachedMutationEntity import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistEntity import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistTrackEntity import com.fabledsword.minstrel.cache.db.entities.CachedQuarantineEntity @@ -46,6 +48,7 @@ import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity CachedPlaylistTrackEntity::class, CachedQuarantineEntity::class, AudioCacheIndexEntity::class, + CachedMutationEntity::class, ], version = 1, exportSchema = true, @@ -61,4 +64,5 @@ abstract class AppDatabase : RoomDatabase() { abstract fun cachedPlaylistTrackDao(): CachedPlaylistTrackDao abstract fun cachedQuarantineDao(): CachedQuarantineDao abstract fun audioCacheIndexDao(): AudioCacheIndexDao + abstract fun cachedMutationDao(): CachedMutationDao } diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedMutationDao.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedMutationDao.kt new file mode 100644 index 00000000..60fd62d9 --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedMutationDao.kt @@ -0,0 +1,34 @@ +package com.fabledsword.minstrel.cache.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import com.fabledsword.minstrel.cache.db.entities.CachedMutationEntity +import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.Instant + +@Dao +interface CachedMutationDao { + /** Pending count for the offline-indicator badge. */ + @Query("SELECT COUNT(*) FROM cached_mutations") + fun observePendingCount(): Flow + + /** FIFO drain order so the replayer processes oldest first. */ + @Query("SELECT * FROM cached_mutations ORDER BY id ASC") + suspend fun getAll(): List + + @Insert + suspend fun insert(row: CachedMutationEntity): Long + + @Query( + "UPDATE cached_mutations SET attempts = attempts + 1, lastAttemptAt = :at " + + "WHERE id = :id", + ) + suspend fun recordAttempt(id: Long, at: Instant) + + @Query("DELETE FROM cached_mutations WHERE id = :id") + suspend fun delete(id: Long) + + @Query("DELETE FROM cached_mutations") + suspend fun clear() +} diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedMutationEntity.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedMutationEntity.kt new file mode 100644 index 00000000..bbc979bb --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedMutationEntity.kt @@ -0,0 +1,32 @@ +package com.fabledsword.minstrel.cache.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant + +/** + * One row per pending offline-write. Mirrors + * `flutter_client/lib/cache/db.dart`'s `CachedMutations` Drift table. + * + * MutationQueue.enqueue() inserts a row when a server-write fails with + * an IOException; MutationReplayer.drain() pops and re-attempts each + * row when connectivity returns (Phase 12.2). + * + * `kind` is a stable string registered in `MutationKind` — the replayer + * looks up the handler in a kind→suspend-function map. Unknown kinds + * are dropped so a malformed row can't wedge the queue forever. + * + * `payload` is the JSON-serialized args needed to replay the REST call. + * Shape is kind-specific; the kind enum doubles as a tag for which + * decoder to apply at drain time. + */ +@Entity(tableName = "cached_mutations") +data class CachedMutationEntity( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + val kind: String, + val payload: String, + val createdAt: Instant = Clock.System.now(), + val lastAttemptAt: Instant? = null, + val attempts: Int = 0, +)