feat(events): A.4 — emit SnapshotChanged from the watchdog probe

Adds a new variant to the lifecycle event catalogue so the UI can
auto-refresh the channel/client tree without an independent polling
timer on the Dart side. The supervisor's existing 5 s snapshot probe
is the source of truth: it already pulls a full snapshot to keep
the watchdog honest, so we piggyback on it.

* `chanora_core::SessionEvent::SnapshotChanged { channels, clients }`
  carries the latest channel and client counts.
* The supervisor compares the probe result to `last_counts` and
  fires the event only when the count actually changes. `last_counts`
  is reset to `None` on a successful reconnect so the freshly
  dialled session re-emits its initial counts.
* `chanora_bridge::api::BridgeEvent::SnapshotChanged` is the
  cross-bridge mirror.
* Flutter routes the event through `_onEvent`, which calls
  `_onRefresh()` to repopulate the snapshot view.

The probe-driven detection has known limits — pure within-channel
client moves do not change the count and so are not surfaced. That
gap will close when the supervisor tracks a content hash in
addition to the count; the count-only signal is sufficient for the
common "someone joined / someone left" case observed on cn.teamspeak.app.
This commit is contained in:
EdisonJwa
2026-05-15 01:27:57 +08:00
parent d2d9ba0a5b
commit 43a3c9ba76
7 changed files with 184 additions and 13 deletions
+29 -1
View File
@@ -123,6 +123,16 @@ pub enum SessionEvent {
/// Audio engine stopped (e.g. before a reconnect cycle, or by
/// explicit user action).
AudioStopped,
/// A snapshot probe observed a change in channel or client
/// counts. Useful for UI auto-refresh without active polling
/// from the Dart side. Carries the counts so subscribers can
/// decide whether to re-fetch.
SnapshotChanged {
/// Number of channels in the latest probe snapshot.
channels: u32,
/// Number of clients in the latest probe snapshot.
clients: u32,
},
}
/// Coarse OS-reported network state. Populated by the Flutter side
@@ -449,6 +459,10 @@ async fn supervisor_loop(
let mut lost_rx = initial_lost_rx;
let mut probe = initial_probe;
let mut cfg = initial_cfg;
// Last snapshot signature observed by the watchdog. Used to
// emit `SessionEvent::SnapshotChanged` only when the channel
// or client counts actually change.
let mut last_counts: Option<(u32, u32)> = None;
loop {
// Watch the current connection: race the protocol task's
@@ -515,7 +529,7 @@ async fn supervisor_loop(
}
_ = watchdog.tick() => {
match tokio::time::timeout(WATCHDOG_PROBE_TIMEOUT, probe.probe()).await {
Ok(Ok(_)) => {
Ok(Ok(snap)) => {
if misses > 0 {
info!(
target: "chanora_core",
@@ -524,6 +538,17 @@ async fn supervisor_loop(
);
}
misses = 0;
// A.4 SnapshotChanged: emit when the
// channel or client count differs from
// the previously observed snapshot.
let counts = (snap.channels.len() as u32, snap.clients.len() as u32);
if last_counts != Some(counts) {
last_counts = Some(counts);
let _ = events_tx.send(SessionEvent::SnapshotChanged {
channels: counts.0,
clients: counts.1,
});
}
}
Ok(Err(e)) => {
misses = misses.saturating_add(1);
@@ -725,6 +750,9 @@ async fn supervisor_loop(
// Loop back to waiting for the next loss.
lost_rx = new_lost_rx;
probe = new_probe;
// Force re-emission of SnapshotChanged
// for the freshly reconnected session.
last_counts = None;
break;
}
Err(e) => {