feat: poll in-flight requests on native + refresh after submit (#369)
Native RequestsViewModel gains poll-while-approved (silent reloads, paused when nothing in-flight) for parity with the web auto-poll, and the SSE collector now reloads silently instead of flashing the loading spinner. Web discover submit invalidates qk.myRequests() so a new request appears on /requests immediately.
This commit is contained in:
+47
-1
@@ -5,11 +5,13 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.events.EventsStream
|
||||
import com.fabledsword.minstrel.models.RequestRef
|
||||
import com.fabledsword.minstrel.models.RequestStatus
|
||||
import com.fabledsword.minstrel.requests.data.CancelOutcome
|
||||
import com.fabledsword.minstrel.requests.data.RequestsRepository
|
||||
import com.fabledsword.minstrel.shared.UiState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -20,6 +22,10 @@ import javax.inject.Inject
|
||||
|
||||
private val RELEVANT_EVENT_KINDS = setOf("request.status_changed")
|
||||
|
||||
// Cadence for in-flight request polling — matches the web client (#369):
|
||||
// fast enough to feel live, slow enough not to hammer the server.
|
||||
private const val POLL_INTERVAL_MS = 12_000L
|
||||
|
||||
@HiltViewModel
|
||||
class RequestsViewModel @Inject constructor(
|
||||
private val repository: RequestsRepository,
|
||||
@@ -31,11 +37,14 @@ class RequestsViewModel @Inject constructor(
|
||||
|
||||
init {
|
||||
refresh()
|
||||
// SSE push: reconciler completions arrive here; reload silently so the
|
||||
// row updates in place rather than flashing the loading spinner.
|
||||
viewModelScope.launch {
|
||||
eventsStream.events
|
||||
.filter { it.kind in RELEVANT_EVENT_KINDS }
|
||||
.collect { refresh() }
|
||||
.collect { silentReload() }
|
||||
}
|
||||
viewModelScope.launch { pollWhileInFlight() }
|
||||
}
|
||||
|
||||
fun refresh(): Job = viewModelScope.launch {
|
||||
@@ -54,6 +63,43 @@ class RequestsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-poll while any request is mid-ingest (status APPROVED), matching the
|
||||
* web client (#369). Reloads silently every [POLL_INTERVAL_MS] and pauses
|
||||
* when nothing is in-flight — complements the SSE push so progress stays
|
||||
* live even if the event stream drops.
|
||||
*/
|
||||
private suspend fun pollWhileInFlight() {
|
||||
while (true) {
|
||||
if (hasInFlightRequest()) {
|
||||
silentReload()
|
||||
}
|
||||
delay(POLL_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasInFlightRequest(): Boolean {
|
||||
val state = internal.value
|
||||
return state is UiState.Success &&
|
||||
state.data.any { it.status == RequestStatus.APPROVED }
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the list in place without flipping to Loading — used by the poll
|
||||
* loop and the SSE collector so visible rows update without a spinner
|
||||
* flash. A transient failure keeps the current view; the next reload wins.
|
||||
*/
|
||||
private suspend fun silentReload() {
|
||||
try {
|
||||
val rows = repository.listMine()
|
||||
internal.value = if (rows.isEmpty()) UiState.Empty else UiState.Success(rows)
|
||||
} catch (
|
||||
@Suppress("TooGenericExceptionCaught", "SwallowedException") e: Throwable,
|
||||
) {
|
||||
// Keep the current view; the next poll / SSE / pull reconciles.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimistically removes [id] from the success list so the row
|
||||
* disappears immediately; on server-side success the row is
|
||||
|
||||
Reference in New Issue
Block a user