feat(android): port cached_mutations (M8 4.2 slice 6)
Mirrors flutter_client/lib/cache/db.dart's CachedMutations — the offline-write queue that MutationQueue.enqueue() inserts into when a server-write fails with IOException and MutationReplayer.drain() pops from when connectivity returns (Phase 12.2). `kind` is a string registered in `MutationKind` (Phase 12.2) so the replayer can map to the right handler. `payload` is JSON-serialized args. Unknown kinds get dropped at drain time rather than wedging. DAO surface tailored to the replayer: - observePendingCount: Flow<Int> for the offline-indicator badge - getAll: FIFO list for drain (id ASC = oldest first) - insert: returns the autoGenerate'd id - recordAttempt(id, instant): atomic increment + lastAttemptAt set - delete(id) / clear Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.CachedAlbumDao
|
||||||
import com.fabledsword.minstrel.cache.db.dao.CachedArtistDao
|
import com.fabledsword.minstrel.cache.db.dao.CachedArtistDao
|
||||||
import com.fabledsword.minstrel.cache.db.dao.CachedLikeDao
|
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.CachedPlaylistDao
|
||||||
import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistTrackDao
|
import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistTrackDao
|
||||||
import com.fabledsword.minstrel.cache.db.dao.CachedQuarantineDao
|
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.CachedAlbumEntity
|
||||||
import com.fabledsword.minstrel.cache.db.entities.CachedArtistEntity
|
import com.fabledsword.minstrel.cache.db.entities.CachedArtistEntity
|
||||||
import com.fabledsword.minstrel.cache.db.entities.CachedLikeEntity
|
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.CachedPlaylistEntity
|
||||||
import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistTrackEntity
|
import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistTrackEntity
|
||||||
import com.fabledsword.minstrel.cache.db.entities.CachedQuarantineEntity
|
import com.fabledsword.minstrel.cache.db.entities.CachedQuarantineEntity
|
||||||
@@ -46,6 +48,7 @@ import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity
|
|||||||
CachedPlaylistTrackEntity::class,
|
CachedPlaylistTrackEntity::class,
|
||||||
CachedQuarantineEntity::class,
|
CachedQuarantineEntity::class,
|
||||||
AudioCacheIndexEntity::class,
|
AudioCacheIndexEntity::class,
|
||||||
|
CachedMutationEntity::class,
|
||||||
],
|
],
|
||||||
version = 1,
|
version = 1,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
@@ -61,4 +64,5 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
abstract fun cachedPlaylistTrackDao(): CachedPlaylistTrackDao
|
abstract fun cachedPlaylistTrackDao(): CachedPlaylistTrackDao
|
||||||
abstract fun cachedQuarantineDao(): CachedQuarantineDao
|
abstract fun cachedQuarantineDao(): CachedQuarantineDao
|
||||||
abstract fun audioCacheIndexDao(): AudioCacheIndexDao
|
abstract fun audioCacheIndexDao(): AudioCacheIndexDao
|
||||||
|
abstract fun cachedMutationDao(): CachedMutationDao
|
||||||
}
|
}
|
||||||
|
|||||||
+34
@@ -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<Int>
|
||||||
|
|
||||||
|
/** FIFO drain order so the replayer processes oldest first. */
|
||||||
|
@Query("SELECT * FROM cached_mutations ORDER BY id ASC")
|
||||||
|
suspend fun getAll(): List<CachedMutationEntity>
|
||||||
|
|
||||||
|
@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()
|
||||||
|
}
|
||||||
Vendored
+32
@@ -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,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user