* feat(ws): consume gateway change events (cron/sessions/platforms/pairing) for targeted refresh (issue #784)
* test(chat): complete catch-all request stub — kill frozen-collector capture race (slash-focus flake, 4645a2b CI)
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
7d60057156
commit
f34bfa7cf8
15 files changed
+320
-4
No files matched your search
@@ -9,7 +9,9 @@ import com.m57.hermescontrol.data.model.TelegramOnboardingStartRequest
|
||||
import com.m57.hermescontrol.data.model.TelegramOnboardingStartResponse
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||
import com.m57.hermescontrol.data.ws.ChangeEvents
|
||||
import com.m57.hermescontrol.ui.common.ToastHost
|
||||
import com.m57.hermescontrol.ui.common.refreshOnChange
|
||||
import com.m57.hermescontrol.ui.common.safeLaunchLoad
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -60,6 +62,24 @@ class ChannelsViewModel :
|
||||
|
||||
private var launchJob: Job? = null
|
||||
|
||||
init {
|
||||
// Issue #784: gateway broadcasts platforms.changed — refresh silently
|
||||
// (no spinner) instead of waiting for a manual pull.
|
||||
refreshOnChange(
|
||||
eventType = ChangeEvents.PLATFORMS,
|
||||
apiCall = { safeApiCall { ApiClient.hermesApi.getMessagingPlatforms() } },
|
||||
onSuccess = { data ->
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
platforms = data.platforms.orEmpty(),
|
||||
gatewayStartCommand = data.gatewayStartCommand,
|
||||
envPath = data.envPath,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun loadPlatforms() {
|
||||
safeLaunchLoad(
|
||||
apiCall = { safeApiCall { ApiClient.hermesApi.getMessagingPlatforms() } },
|
||||
|
||||
@@ -130,6 +130,9 @@ object ChatWsEventReducer {
|
||||
|
||||
// ReactionEvent is handled by the ViewModel — purely cosmetic animation
|
||||
is WsEvent.ReactionEvent -> ReducerResult(state = state, streamingState = streamingState)
|
||||
|
||||
// Change events (issue #784) are consumed by their screens' ViewModels
|
||||
is WsEvent.ChangeEvent -> ReducerResult(state = state, streamingState = streamingState)
|
||||
}
|
||||
|
||||
// ── GatewayReady ──────────────────────────────────────────────────
|
||||
|
||||
@@ -3,7 +3,9 @@ package com.m57.hermescontrol.ui.common
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||
import com.m57.hermescontrol.data.ws.ChangeEventHub
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
interface ToastHost {
|
||||
@@ -31,3 +33,41 @@ inline fun <T> ViewModel.safeLaunchLoad(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect a gateway change event ([com.m57.hermescontrol.data.ws.ChangeEvents])
|
||||
* from [ChangeEventHub] and run a **silent** refresh: no loading spinner, no
|
||||
* error surface — stale data stays in place on failure. At most one request
|
||||
* runs at a time; events arriving mid-flight are skipped, so a
|
||||
* `sessions.changed` burst during a long streaming turn (the gateway floors
|
||||
* those at 2s) cannot pile up requests.
|
||||
*
|
||||
* Call from `init`. Backends without `change_events` never broadcast, and in
|
||||
* unit tests the hub is never fed (HermesWsClient is mocked or uninitialized),
|
||||
* so this is a no-op there — exactly the "slow backstop" contract of issue
|
||||
* #784.
|
||||
*/
|
||||
inline fun <T> ViewModel.refreshOnChange(
|
||||
eventType: String,
|
||||
crossinline apiCall: suspend () -> NetworkResult<T>,
|
||||
crossinline onSuccess: (T) -> Unit,
|
||||
): Job {
|
||||
var refreshInFlight = false
|
||||
return viewModelScope.launch {
|
||||
ChangeEventHub.events
|
||||
.filter { it.type == eventType }
|
||||
.collect { _ ->
|
||||
if (refreshInFlight) return@collect
|
||||
refreshInFlight = true
|
||||
try {
|
||||
// No Dispatchers.IO hop — see safeLaunchLoad.
|
||||
val result = apiCall()
|
||||
if (result is NetworkResult.Success) {
|
||||
onSuccess(result.data)
|
||||
}
|
||||
} finally {
|
||||
refreshInFlight = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,10 @@ import com.m57.hermescontrol.data.model.UpdateCronJobRequest
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||
import com.m57.hermescontrol.data.ws.ChangeEvents
|
||||
import com.m57.hermescontrol.data.ws.toJsonElement
|
||||
import com.m57.hermescontrol.ui.common.ToastHost
|
||||
import com.m57.hermescontrol.ui.common.refreshOnChange
|
||||
import com.m57.hermescontrol.ui.common.safeLaunchLoad
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -83,6 +85,16 @@ class CronJobsViewModel :
|
||||
|
||||
private var loadJob: Job? = null
|
||||
|
||||
init {
|
||||
// Issue #784: gateway broadcasts cron.changed — refresh the list
|
||||
// silently on change instead of waiting for a manual pull.
|
||||
refreshOnChange(
|
||||
eventType = ChangeEvents.CRON,
|
||||
apiCall = { safeApiCall { ApiClient.hermesApi.getCronJobs() } },
|
||||
onSuccess = { data -> _uiState.update { it.copy(jobs = data.orEmpty()) } },
|
||||
)
|
||||
}
|
||||
|
||||
fun loadCronJobs() {
|
||||
loadJob =
|
||||
safeLaunchLoad(
|
||||
|
||||
@@ -8,7 +8,9 @@ import com.m57.hermescontrol.data.model.PairingRevokeRequest
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||
import com.m57.hermescontrol.data.ws.ChangeEvents
|
||||
import com.m57.hermescontrol.ui.common.ToastHost
|
||||
import com.m57.hermescontrol.ui.common.refreshOnChange
|
||||
import com.m57.hermescontrol.ui.common.safeLaunchLoad
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -36,6 +38,16 @@ class PairingViewModel :
|
||||
|
||||
private var launchJob: Job? = null
|
||||
|
||||
init {
|
||||
// Issue #784: gateway broadcasts pairing.changed — refresh silently
|
||||
// (no spinner) instead of waiting for a manual pull.
|
||||
refreshOnChange(
|
||||
eventType = ChangeEvents.PAIRING,
|
||||
apiCall = { safeApiCall { ApiClient.hermesApi.getPairing() } },
|
||||
onSuccess = { data -> _uiState.update { it.copy(pairing = data) } },
|
||||
)
|
||||
}
|
||||
|
||||
fun loadPairing() {
|
||||
launchJob =
|
||||
safeLaunchLoad(
|
||||
|
||||
@@ -10,7 +10,9 @@ import com.m57.hermescontrol.data.model.SessionSearchResult
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||
import com.m57.hermescontrol.data.ws.ChangeEvents
|
||||
import com.m57.hermescontrol.ui.common.ToastHost
|
||||
import com.m57.hermescontrol.ui.common.refreshOnChange
|
||||
import com.m57.hermescontrol.ui.common.safeLaunchLoad
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -62,6 +64,31 @@ class SessionsViewModel :
|
||||
private var loadJob: Job? = null
|
||||
private var statsJob: Job? = null
|
||||
|
||||
init {
|
||||
// Issue #784: gateway broadcasts sessions.changed — refresh the list
|
||||
// silently (no spinner, no selection reset) instead of blind polling.
|
||||
refreshOnChange(
|
||||
eventType = ChangeEvents.SESSIONS,
|
||||
apiCall = {
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.getSessions(
|
||||
limit = PAGE_SIZE,
|
||||
offset = 0,
|
||||
order = "recent",
|
||||
)
|
||||
}
|
||||
},
|
||||
onSuccess = { data ->
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
sessions = data.sessions.orEmpty(),
|
||||
total = data.total,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** Page size sent to the server — matches the default the gateway uses. */
|
||||
private companion object {
|
||||
const val PAGE_SIZE = 20
|
||||
|
||||
Reference in new issue
Block a user