feat(core): A.6 — supervisor reconnect with watchdog and event stream
Adds an end-to-end auto-reconnect path so a brief network outage no longer leaves the client wedged in a half-dead state. The flow has three layers, each motivated by a real failure mode observed on the Moto G live test: * `chanora_protocol::DisconnectReason` (`UserRequested` / `StreamEnded` / `Error(String)`) is reported on a `oneshot` when the per-connection task exits, so the supervisor can tell user intent apart from a real loss. * `chanora_core` spawns a supervisor task per `ChanoraSession`. It listens for the loss notifier AND runs a watchdog that issues `snapshot()` probes every 5s with a 4s timeout — three consecutive misses synthesise a `DisconnectReason::Error(...)` and trigger the reconnect path. The watchdog catches the "ghost connected" case where tsclientlib silently resets internal state but the event stream never errors. Backoff schedule: 1s, 2s, 5s, 15s, 30s, 60s (capped). On success the supervisor swaps the dead `ProtocolClient` for the new one in place and, if audio was running, restarts the audio engine bound to the new `voice_in`/`voice_out` channels. * `SessionEvent` (Connected / Lost / Reconnecting / Disconnected / AudioStarted / AudioStopped) is broadcast on a 64-slot channel. `chanora_bridge` re-exports it as `BridgeEvent` and exposes `events_stream(StreamSink)`; the Flutter side subscribes from `initState` and renders a reconnect banner with attempt count and delay. New `SnapshotProbe` exposes a clone-friendly snapshot path so the watchdog can probe without holding `&self` across awaits. Localization adds `statusReconnecting` and `statusConnectionLost` keys to `app_en.arb` and `app_zh.arb`. Verified on Moto G Stylus 5G (Android 14) against cn.teamspeak.app: killed Wi-Fi + cellular for ~70 s; watchdog declared loss at three misses, supervisor walked the backoff schedule, and the UI reconnected automatically once the radios came back. Snapshot tree re-rendered without user action.
This commit is contained in:
@@ -27,6 +27,18 @@
|
||||
"placeholders": { "message": { "type": "String" } }
|
||||
},
|
||||
|
||||
"statusReconnecting": "Reconnecting… attempt {attempt}, in {delay}s",
|
||||
"@statusReconnecting": {
|
||||
"placeholders": {
|
||||
"attempt": { "type": "int" },
|
||||
"delay": { "type": "int" }
|
||||
}
|
||||
},
|
||||
"statusConnectionLost": "Connection lost: {reason}",
|
||||
"@statusConnectionLost": {
|
||||
"placeholders": { "reason": { "type": "String" } }
|
||||
},
|
||||
|
||||
"audioStatsLine": "TX {sent} frames • RX {received} frames • PTT {ptt}",
|
||||
"@audioStatsLine": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
"statusConnected": "已连接到 {server}",
|
||||
"statusError": "错误:{message}",
|
||||
|
||||
"statusReconnecting": "正在重新连接……第 {attempt} 次尝试,{delay} 秒后",
|
||||
"statusConnectionLost": "连接已断开:{reason}",
|
||||
|
||||
"audioStatsLine": "发送 {sent} 帧 • 接收 {received} 帧 • PTT {ptt}",
|
||||
|
||||
"channelsHeading": "频道",
|
||||
|
||||
@@ -181,6 +181,18 @@ abstract class AppL10n {
|
||||
/// **'Error: {message}'**
|
||||
String statusError(String message);
|
||||
|
||||
/// No description provided for @statusReconnecting.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reconnecting… attempt {attempt}, in {delay}s'**
|
||||
String statusReconnecting(int attempt, int delay);
|
||||
|
||||
/// No description provided for @statusConnectionLost.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Connection lost: {reason}'**
|
||||
String statusConnectionLost(String reason);
|
||||
|
||||
/// No description provided for @audioStatsLine.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -55,6 +55,16 @@ class AppL10nEn extends AppL10n {
|
||||
return 'Error: $message';
|
||||
}
|
||||
|
||||
@override
|
||||
String statusReconnecting(int attempt, int delay) {
|
||||
return 'Reconnecting… attempt $attempt, in ${delay}s';
|
||||
}
|
||||
|
||||
@override
|
||||
String statusConnectionLost(String reason) {
|
||||
return 'Connection lost: $reason';
|
||||
}
|
||||
|
||||
@override
|
||||
String audioStatsLine(int sent, int received, String ptt) {
|
||||
return 'TX $sent frames • RX $received frames • PTT $ptt';
|
||||
|
||||
@@ -54,6 +54,16 @@ class AppL10nZh extends AppL10n {
|
||||
return '错误:$message';
|
||||
}
|
||||
|
||||
@override
|
||||
String statusReconnecting(int attempt, int delay) {
|
||||
return '正在重新连接……第 $attempt 次尝试,$delay 秒后';
|
||||
}
|
||||
|
||||
@override
|
||||
String statusConnectionLost(String reason) {
|
||||
return '连接已断开:$reason';
|
||||
}
|
||||
|
||||
@override
|
||||
String audioStatsLine(int sent, int received, String ptt) {
|
||||
return '发送 $sent 帧 • 接收 $received 帧 • PTT $ptt';
|
||||
|
||||
Reference in New Issue
Block a user