Coverage Summary for Class: PendingMutationQueue (com.georgeci.moneysurfer.sync.repository)

Class Method, % Branch, % Line, % Instruction, %
PendingMutationQueue 100% (2/2) 100% (2/2) 100% (34/34)
PendingMutationQueue$Companion
PendingMutationQueue$DefaultImpls
Total 100% (2/2) 100% (2/2) 100% (34/34)


 package com.georgeci.moneysurfer.sync.repository
 
 import com.georgeci.moneysurfer.sync.api.SyncScope
 import kotlinx.coroutines.flow.Flow
 
 /**
  * Outbox of local mutations waiting to be pushed to the remote backend.
  *
  * Lifecycle of a row:
  * 1. [enqueue] — produced by a repository's dual-write, right after the local write. **Not** in the
  *    same transaction, and no transaction can make it so: the outbox lives in `SyncDatabase`
  *    (`moneysurfer_sync.db`), a different database from `MoneySurferDatabase`, so the two writes are
  *    always two sequential calls and a crash in between leaves a local change with nothing queued
  *    for it. What closes that window is reconciliation at session start, not atomicity.
  *    Enqueueing is insert-if-absent among `PENDING` rows with the same type, id, operation **and
  *    scope**, so repeated edits of one entity queue one push rather than N identical ones while the
  *    same id under two workspaces still queues two. Status = `PENDING`.
  * 2. [pending] — read by [com.georgeci.moneysurfer.sync.usecase.UploadPendingChangesUseCase]
  *    when sync runs. Caller then flips status via [markInFlight].
  * 3. [markCompleted] — successful push removes the row.
  * 4. [markFailed] — push exception bumps `attempts` and resets status to `PENDING`
  *    (next sync cycle picks it up again, possibly with backoff in a later phase).
  *
  * See SyncCoordinatorFAQ.md №13.
  */
 interface PendingMutationQueue {
     suspend fun enqueue(mutation: PendingMutation)
 
     suspend fun pending(scope: SyncScope, limit: Int = DEFAULT_BATCH_LIMIT): List<PendingMutation>
 
     suspend fun markInFlight(ids: List<String>)
 
     suspend fun markCompleted(ids: List<String>)
 
     suspend fun markFailed(id: String, error: String)
 
     val pendingCount: Flow<Int>
 
     /**
      * Live view of the outbox rows themselves, oldest first — the Settings → Sync screen's
      * outbox section. Unlike [pendingCount] this includes rows currently `IN_FLIGHT`: a push
      * that never completes leaves the row in that status, and hiding it would make a stuck
      * outbox look like an empty one.
      *
      * Diagnostics only — the push path reads [pending] instead, because it needs the scope
      * filter and the `markInFlight` handshake that goes with it.
      */
     fun observeOutbox(limit: Int = DEFAULT_OUTBOX_LIMIT): Flow<List<PendingMutation>>
 
     companion object {
         const val DEFAULT_BATCH_LIMIT: Int = 100
 
         /** Enough rows to diagnose a stuck outbox without rendering thousands of them. */
         const val DEFAULT_OUTBOX_LIMIT: Int = 50
     }
 }