feat(core): A.6.1 — use OS connectivity signals to drive reconnect

Extends the A.6 supervisor with an OS-level connectivity hint so a
returning network triggers a redial immediately instead of waiting
out the current backoff slot (up to 60 s). The watchdog remains the
authoritative loss detector — the OS signal is advisory.

* `chanora_core::NetworkState` (Unknown / Online / Offline) is owned
  by `ChanoraSession` via a `tokio::sync::watch::Sender`.
  `set_network_state()` / `network_state()` are the public accessors.
* The supervisor's watch-phase `select!` gains a `network_rx`
  branch: Offline pre-charges watchdog misses (capped at
  `MAX_MISSES - 1`) so the next probe failure trips immediately;
  Online clears stale misses. This shrinks UI-banner latency on a
  Wi-Fi drop from ~15 s to ~5 s.
* The reconnect-loop's backoff sleep races against Online: a
  transition cuts the sleep short and resets the attempt counter so
  future losses start at the smallest backoff window again.
* `chanora_bridge` adds `BridgeNetworkState` (mirror enum) and a
  sync `set_network_state(state)` function. On platforms with no
  signal wired the supervisor stays at Unknown and falls back to
  pure watchdog/backoff — no behavioural regression vs A.6.
* Flutter adds `connectivity_plus ^6.1.0` and wires
  `_wireConnectivity()` in `main()`: seeds with `checkConnectivity()`
  then forwards every `onConnectivityChanged` to the bridge,
  mapping any non-`none` transport to Online.

Verified on Moto G Stylus 5G (Android 14): `svc wifi disable && svc
data disable` for ~40 s — reconnect banner appeared promptly
because the watchdog was pre-charged. After `svc wifi enable && svc
data enable` the supervisor woke from its 15 s backoff slot and
reconnected within seconds; the channel tree re-rendered without
user action.
This commit is contained in:
EdisonJwa
2026-05-15 01:07:21 +08:00
parent 0bef61aea2
commit f52d702e27
10 changed files with 485 additions and 63 deletions
+21 -1
View File
@@ -10,7 +10,7 @@ import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
part 'api.freezed.dart';
// These functions are ignored because they are not marked as `pub`: `runtime`, `session`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`
/// Connect to a TeamSpeak-compatible server and return the initial
/// state snapshot. Honours the DEC-006 single-connection invariant
@@ -37,6 +37,13 @@ Future<void> startAudio() => RustLib.instance.api.crateApiStartAudio();
Future<void> setPtt({required bool active}) =>
RustLib.instance.api.crateApiSetPtt(active: active);
/// Notify the core of the latest OS-reported connectivity state.
/// Called by the Flutter side from `connectivity_plus` callbacks.
/// The core's supervisor uses this to (a) pre-charge the watchdog
/// on Offline and (b) short-circuit reconnect backoff on Online.
void setNetworkState({required BridgeNetworkState state}) =>
RustLib.instance.api.crateApiSetNetworkState(state: state);
/// Subscribe to lifecycle events. Each call yields a fresh
/// subscription; multiple subscribers are supported. On slow
/// consumers, events are dropped rather than blocking the supervisor
@@ -185,6 +192,19 @@ sealed class BridgeEvent with _$BridgeEvent {
const factory BridgeEvent.audioStopped() = BridgeEvent_AudioStopped;
}
/// Coarse OS-reported network state. Mirrors
/// [`chanora_core::NetworkState`] across the bridge.
enum BridgeNetworkState {
/// No signal seen yet.
unknown,
/// OS reports a usable network.
online,
/// OS reports no network.
offline,
}
/// Server snapshot as seen by Dart.
class BridgeSnapshot {
/// Server name.
@@ -67,7 +67,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.12.0';
@override
int get rustContentHash => -1896742393;
int get rustContentHash => -1212711005;
static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig(
@@ -94,6 +94,8 @@ abstract class RustLibApi extends BaseApi {
Future<bool> crateApiIsConnected();
void crateApiSetNetworkState({required BridgeNetworkState state});
Future<void> crateApiSetPtt({required bool active});
Future<BridgeSnapshot> crateApiSnapshot();
@@ -281,6 +283,29 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateApiIsConnectedConstMeta =>
const TaskConstMeta(debugName: "is_connected", argNames: []);
@override
void crateApiSetNetworkState({required BridgeNetworkState state}) {
return handler.executeSync(
SyncTask(
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_bridge_network_state(state, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 7)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
decodeErrorData: null,
),
constMeta: kCrateApiSetNetworkStateConstMeta,
argValues: [state],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiSetNetworkStateConstMeta =>
const TaskConstMeta(debugName: "set_network_state", argNames: ["state"]);
@override
Future<void> crateApiSetPtt({required bool active}) {
return handler.executeNormal(
@@ -291,7 +316,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 7,
funcId: 8,
port: port_,
);
},
@@ -318,7 +343,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 8,
funcId: 9,
port: port_,
);
},
@@ -345,7 +370,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 9,
funcId: 10,
port: port_,
);
},
@@ -477,6 +502,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
@protected
BridgeNetworkState dco_decode_bridge_network_state(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return BridgeNetworkState.values[raw as int];
}
@protected
BridgeSnapshot dco_decode_bridge_snapshot(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -493,6 +524,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
);
}
@protected
int dco_decode_i_32(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return raw as int;
}
@protected
PlatformInt64 dco_decode_i_64(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -665,6 +702,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
@protected
BridgeNetworkState sse_decode_bridge_network_state(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
var inner = sse_decode_i_32(deserializer);
return BridgeNetworkState.values[inner];
}
@protected
BridgeSnapshot sse_decode_bridge_snapshot(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -684,6 +730,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
);
}
@protected
int sse_decode_i_32(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
return deserializer.buffer.getInt32();
}
@protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -748,12 +800,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
// Codec=Sse (Serialization based), see doc to use other codecs
}
@protected
int sse_decode_i_32(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
return deserializer.buffer.getInt32();
}
@protected
void sse_encode_AnyhowException(
AnyhowException self,
@@ -871,6 +917,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
@protected
void sse_encode_bridge_network_state(
BridgeNetworkState self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_i_32(self.index, serializer);
}
@protected
void sse_encode_bridge_snapshot(
BridgeSnapshot self,
@@ -885,6 +940,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_list_bridge_client(self.clients, serializer);
}
@protected
void sse_encode_i_32(int self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
serializer.buffer.putInt32(self);
}
@protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -947,10 +1008,4 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
void sse_encode_unit(void self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
}
@protected
void sse_encode_i_32(int self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
serializer.buffer.putInt32(self);
}
}
@@ -48,9 +48,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BridgeEvent dco_decode_bridge_event(dynamic raw);
@protected
BridgeNetworkState dco_decode_bridge_network_state(dynamic raw);
@protected
BridgeSnapshot dco_decode_bridge_snapshot(dynamic raw);
@protected
int dco_decode_i_32(dynamic raw);
@protected
PlatformInt64 dco_decode_i_64(dynamic raw);
@@ -104,9 +110,17 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BridgeEvent sse_decode_bridge_event(SseDeserializer deserializer);
@protected
BridgeNetworkState sse_decode_bridge_network_state(
SseDeserializer deserializer,
);
@protected
BridgeSnapshot sse_decode_bridge_snapshot(SseDeserializer deserializer);
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
@@ -135,9 +149,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_decode_unit(SseDeserializer deserializer);
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@protected
void sse_encode_AnyhowException(
AnyhowException self,
@@ -174,12 +185,21 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_bridge_event(BridgeEvent self, SseSerializer serializer);
@protected
void sse_encode_bridge_network_state(
BridgeNetworkState self,
SseSerializer serializer,
);
@protected
void sse_encode_bridge_snapshot(
BridgeSnapshot self,
SseSerializer serializer,
);
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
@protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
@@ -212,9 +232,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_unit(void self, SseSerializer serializer);
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
}
// Section: wire_class
@@ -50,9 +50,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BridgeEvent dco_decode_bridge_event(dynamic raw);
@protected
BridgeNetworkState dco_decode_bridge_network_state(dynamic raw);
@protected
BridgeSnapshot dco_decode_bridge_snapshot(dynamic raw);
@protected
int dco_decode_i_32(dynamic raw);
@protected
PlatformInt64 dco_decode_i_64(dynamic raw);
@@ -106,9 +112,17 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BridgeEvent sse_decode_bridge_event(SseDeserializer deserializer);
@protected
BridgeNetworkState sse_decode_bridge_network_state(
SseDeserializer deserializer,
);
@protected
BridgeSnapshot sse_decode_bridge_snapshot(SseDeserializer deserializer);
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
@@ -137,9 +151,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_decode_unit(SseDeserializer deserializer);
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@protected
void sse_encode_AnyhowException(
AnyhowException self,
@@ -176,12 +187,21 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_bridge_event(BridgeEvent self, SseSerializer serializer);
@protected
void sse_encode_bridge_network_state(
BridgeNetworkState self,
SseSerializer serializer,
);
@protected
void sse_encode_bridge_snapshot(
BridgeSnapshot self,
SseSerializer serializer,
);
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
@protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
@@ -214,9 +234,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_unit(void self, SseSerializer serializer);
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
}
// Section: wire_class