feat(ws): consume gateway change events (issue #784) (#837)

* 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:
M57andM57 authored and GitHub committed 2026-08-07 13:48:17 +03:00
1 parent 7d60057156
commit f34bfa7cf8
15 files changed
+320 -4

No files matched your search

@@ -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
}
}
}
}