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:
@@ -233,6 +233,39 @@ pub struct BridgeAudioStats {
|
||||
pub ptt_active: bool,
|
||||
}
|
||||
|
||||
// ---------- Connectivity (A.6.1) ----------
|
||||
|
||||
/// Coarse OS-reported network state. Mirrors
|
||||
/// [`chanora_core::NetworkState`] across the bridge.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum BridgeNetworkState {
|
||||
/// No signal seen yet.
|
||||
Unknown,
|
||||
/// OS reports a usable network.
|
||||
Online,
|
||||
/// OS reports no network.
|
||||
Offline,
|
||||
}
|
||||
|
||||
impl From<BridgeNetworkState> for chanora_core::NetworkState {
|
||||
fn from(s: BridgeNetworkState) -> Self {
|
||||
match s {
|
||||
BridgeNetworkState::Unknown => chanora_core::NetworkState::Unknown,
|
||||
BridgeNetworkState::Online => chanora_core::NetworkState::Online,
|
||||
BridgeNetworkState::Offline => chanora_core::NetworkState::Offline,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[frb(sync)]
|
||||
pub fn set_network_state(state: BridgeNetworkState) {
|
||||
session().set_network_state(state.into());
|
||||
}
|
||||
|
||||
// ---------- Events (A.6) ----------
|
||||
|
||||
/// Lifecycle event surfaced to Dart. Schema-controlled mirror of
|
||||
|
||||
@@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||
);
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1896742393;
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1212711005;
|
||||
|
||||
// Section: executor
|
||||
|
||||
@@ -258,6 +258,38 @@ fn wire__crate__api__is_connected_impl(
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__set_network_state_impl(
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
|
||||
flutter_rust_bridge::for_generated::TaskInfo {
|
||||
debug_name: "set_network_state",
|
||||
port: None,
|
||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
|
||||
},
|
||||
move || {
|
||||
let message = unsafe {
|
||||
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||
ptr_,
|
||||
rust_vec_len_,
|
||||
data_len_,
|
||||
)
|
||||
};
|
||||
let mut deserializer =
|
||||
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||
let api_state = <crate::api::BridgeNetworkState>::sse_decode(&mut deserializer);
|
||||
deserializer.end();
|
||||
transform_result_sse::<_, ()>((move || {
|
||||
let output_ok = Result::<_, ()>::Ok({
|
||||
crate::api::set_network_state(api_state);
|
||||
})?;
|
||||
Ok(output_ok)
|
||||
})())
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__set_ptt_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
@@ -522,6 +554,19 @@ impl SseDecode for crate::api::BridgeEvent {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for crate::api::BridgeNetworkState {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut inner = <i32>::sse_decode(deserializer);
|
||||
return match inner {
|
||||
0 => crate::api::BridgeNetworkState::Unknown,
|
||||
1 => crate::api::BridgeNetworkState::Online,
|
||||
2 => crate::api::BridgeNetworkState::Offline,
|
||||
_ => unreachable!("Invalid variant for BridgeNetworkState: {}", inner),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for crate::api::BridgeSnapshot {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
@@ -542,6 +587,13 @@ impl SseDecode for crate::api::BridgeSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for i32 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
deserializer.cursor.read_i32::<NativeEndian>().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for i64 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
@@ -611,13 +663,6 @@ impl SseDecode for () {
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {}
|
||||
}
|
||||
|
||||
impl SseDecode for i32 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
deserializer.cursor.read_i32::<NativeEndian>().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn pde_ffi_dispatcher_primary_impl(
|
||||
func_id: i32,
|
||||
port: flutter_rust_bridge::for_generated::MessagePort,
|
||||
@@ -633,9 +678,9 @@ fn pde_ffi_dispatcher_primary_impl(
|
||||
4 => wire__crate__api__disconnect_impl(port, ptr, rust_vec_len, data_len),
|
||||
5 => wire__crate__api__events_stream_impl(port, ptr, rust_vec_len, data_len),
|
||||
6 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len),
|
||||
7 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len),
|
||||
8 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len),
|
||||
9 => wire__crate__api__start_audio_impl(port, ptr, rust_vec_len, data_len),
|
||||
8 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len),
|
||||
9 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len),
|
||||
10 => wire__crate__api__start_audio_impl(port, ptr, rust_vec_len, data_len),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -648,6 +693,7 @@ fn pde_ffi_dispatcher_sync_impl(
|
||||
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
||||
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||
match func_id {
|
||||
7 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -778,6 +824,28 @@ impl flutter_rust_bridge::IntoIntoDart<crate::api::BridgeEvent> for crate::api::
|
||||
}
|
||||
}
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::api::BridgeNetworkState {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
match self {
|
||||
Self::Unknown => 0.into_dart(),
|
||||
Self::Online => 1.into_dart(),
|
||||
Self::Offline => 2.into_dart(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
|
||||
for crate::api::BridgeNetworkState
|
||||
{
|
||||
}
|
||||
impl flutter_rust_bridge::IntoIntoDart<crate::api::BridgeNetworkState>
|
||||
for crate::api::BridgeNetworkState
|
||||
{
|
||||
fn into_into_dart(self) -> crate::api::BridgeNetworkState {
|
||||
self
|
||||
}
|
||||
}
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::api::BridgeSnapshot {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
[
|
||||
@@ -927,6 +995,23 @@ impl SseEncode for crate::api::BridgeEvent {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for crate::api::BridgeNetworkState {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<i32>::sse_encode(
|
||||
match self {
|
||||
crate::api::BridgeNetworkState::Unknown => 0,
|
||||
crate::api::BridgeNetworkState::Online => 1,
|
||||
crate::api::BridgeNetworkState::Offline => 2,
|
||||
_ => {
|
||||
unimplemented!("");
|
||||
}
|
||||
},
|
||||
serializer,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for crate::api::BridgeSnapshot {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
@@ -939,6 +1024,13 @@ impl SseEncode for crate::api::BridgeSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for i32 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
serializer.cursor.write_i32::<NativeEndian>(self).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for i64 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
@@ -1002,13 +1094,6 @@ impl SseEncode for () {
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {}
|
||||
}
|
||||
|
||||
impl SseEncode for i32 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
serializer.cursor.write_i32::<NativeEndian>(self).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
mod io {
|
||||
// This file is automatically generated, so please do not edit it.
|
||||
|
||||
Reference in New Issue
Block a user