feat(alpha): wire connect→snapshot→disconnect end-to-end (v0.1.0-alpha.1)
First Internal Alpha build per DEC-001. Closes the milestone of
'Flutter UI calls Rust via the typed bridge, Rust connects to a
TeamSpeak-compatible server through tsclientlib, returns a typed
snapshot, and disconnects cleanly.' Audio remains Beta scope.
Promotions from PoC:
poc/tsclientlib-connect-spike → crates/chanora_protocol/
New product code:
crates/chanora_protocol/src/{dto.rs,adapter.rs} — typed boundary
over tsclientlib. Tokio task owns the Connection; public
handle communicates via mpsc/oneshot. No tsclientlib types
cross out of the crate (SAD-067 / SysDes-011 / SysDes-029).
core/chanora_core/src/lib.rs — ChanoraSession composes the
protocol crate, enforces the DEC-006 single-connection
invariant.
crates/chanora_bridge/src/{api.rs,frb_generated.rs} — FRB 2.12.0
bridge per DEC-014. cdylib + staticlib + rlib. Typed
BridgeSnapshot / BridgeChannel / BridgeClient / BridgeError
DTOs. Process-wide OnceLock<Runtime> + OnceLock<ChanoraSession>.
flutter_rust_bridge.yaml at repo root.
apps/chanora_flutter/lib/main.dart — Alpha UI: server form,
connect button, channel tree, disconnect.
apps/chanora_flutter/lib/l10n/app_{en,zh}.arb expanded with the
Alpha key set; ARB metadata reaffirms ADR-008 for
server-provided content.
Generated Dart bindings under apps/chanora_flutter/lib/src/rust/.
Empirical verification (2026-05-14):
Workspace: cargo check + cargo test clean
(workspace tests: all green).
Bridge cdylib: target/release/libchanora_bridge.so produced
(~15 MB).
Flutter: flutter analyze clean; flutter test runs 3/3 green
including the alpha_e2e_test that drives the full
Dart → FRB → chanora_bridge → chanora_core → chanora_protocol
→ tsclientlib → UDP → cn.teamspeak.app
path. The captured logcat/stdout shows the tsproto resender
transitioning Connected → Disconnecting → Disconnected on
clean teardown.
Architecture changes:
- Removed the chanora_core ↔ chanora_bridge cyclic dependency.
chanora_core no longer knows the bridge exists; the bridge
maps from CoreError.
- chanora_bridge crate's #![forbid(unsafe_code)] lint relaxed
because FRB-generated glue legitimately uses unsafe at the
FFI boundary. Hand-written code remains unsafe-free.
Open follow-ups (NOT in this Alpha):
- Audio capture/playback wiring into chanora_audio
(Beta scope per DEC-001).
- Identity persistence via chanora_storage
(currently regenerated on every connect).
- Per-message diagnostics + redaction
(chanora_diagnostics still scaffold).
- Reconnect / network-loss recovery.
- Mobile (Android) build of the bridge cdylib + UI verification.
This commit is contained in:
@@ -9,7 +9,21 @@ license.workspace = true
|
||||
repository.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[lib]
|
||||
# cdylib so the Flutter app can dlopen us via dart:ffi.
|
||||
# staticlib so iOS / static-link configurations remain possible later.
|
||||
# rlib so chanora_core and other Rust callers can use the public types.
|
||||
crate-type = ["cdylib", "staticlib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
chanora_core = { path = "../../core/chanora_core" }
|
||||
chanora_protocol = { path = "../chanora_protocol" }
|
||||
flutter_rust_bridge = "=2.12.0"
|
||||
thiserror.workspace = true
|
||||
serde.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] }
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
//! Public API exposed to Dart via `flutter_rust_bridge`.
|
||||
//!
|
||||
//! Naming follows the FRB v2 convention: free functions at the crate
|
||||
//! API root, with `#[frb(sync)]` for synchronous calls and async fn
|
||||
//! signatures for async ones. Every input and output is an owned
|
||||
//! type whose layout is schema-controlled (no `tsclientlib`, no
|
||||
//! `cpal`, no `Connection` handles).
|
||||
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
|
||||
use flutter_rust_bridge::frb;
|
||||
use tokio::runtime::Runtime;
|
||||
use tracing::info;
|
||||
|
||||
use crate::BridgeError;
|
||||
|
||||
/// Process-wide tokio runtime used to drive the async core. Created
|
||||
/// lazily on first use and never torn down — the application's
|
||||
/// process lifetime is the runtime's lifetime.
|
||||
fn runtime() -> &'static Runtime {
|
||||
static RT: OnceLock<Runtime> = OnceLock::new();
|
||||
RT.get_or_init(|| {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(2)
|
||||
.enable_all()
|
||||
.thread_name("chanora-rt")
|
||||
.build()
|
||||
.expect("tokio runtime")
|
||||
})
|
||||
}
|
||||
|
||||
/// Process-wide session handle. One instance per process is enough
|
||||
/// for Alpha (DEC-006 single-connection invariant); the Mutex inside
|
||||
/// `ChanoraSession` enforces the connection-count invariant.
|
||||
fn session() -> &'static chanora_core::ChanoraSession {
|
||||
static S: OnceLock<chanora_core::ChanoraSession> = OnceLock::new();
|
||||
S.get_or_init(chanora_core::ChanoraSession::new)
|
||||
}
|
||||
|
||||
// ---------- Bridge lifecycle ----------
|
||||
|
||||
/// Initialise the bridge. Must be called once on Dart side before
|
||||
/// any other API call. Sets up panic logging.
|
||||
#[frb(init)]
|
||||
pub fn bridge_init() {
|
||||
flutter_rust_bridge::setup_default_user_utils();
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
)
|
||||
.with_target(true)
|
||||
.try_init();
|
||||
info!(target: "chanora_bridge", "bridge initialised");
|
||||
}
|
||||
|
||||
// ---------- DTOs ----------
|
||||
|
||||
/// Channel as seen by Dart. Matches `chanora_protocol::ChannelInfo`
|
||||
/// but with primitive `u64` ids so the Dart side gets `BigInt`s
|
||||
/// without any wrapper-type ceremony.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BridgeChannel {
|
||||
/// Stable channel id.
|
||||
pub id: u64,
|
||||
/// Parent channel id; 0 means top-level.
|
||||
pub parent: u64,
|
||||
/// Display name.
|
||||
pub name: String,
|
||||
/// Server-side ordering hint.
|
||||
pub order: i64,
|
||||
}
|
||||
|
||||
/// Client as seen by Dart.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BridgeClient {
|
||||
/// Stable client id.
|
||||
pub id: u64,
|
||||
/// Channel id the client is currently in.
|
||||
pub channel: u64,
|
||||
/// Nickname.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Server snapshot as seen by Dart.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BridgeSnapshot {
|
||||
/// Server name.
|
||||
pub server_name: String,
|
||||
/// Welcome banner text.
|
||||
pub welcome_message: String,
|
||||
/// Server platform (e.g. "Linux").
|
||||
pub platform: String,
|
||||
/// Server version string.
|
||||
pub version: String,
|
||||
/// Channels currently known.
|
||||
pub channels: Vec<BridgeChannel>,
|
||||
/// Clients currently known.
|
||||
pub clients: Vec<BridgeClient>,
|
||||
}
|
||||
|
||||
impl From<chanora_protocol::ServerSnapshot> for BridgeSnapshot {
|
||||
fn from(s: chanora_protocol::ServerSnapshot) -> Self {
|
||||
Self {
|
||||
server_name: s.server_name,
|
||||
welcome_message: s.welcome_message,
|
||||
platform: s.platform,
|
||||
version: s.version,
|
||||
channels: s
|
||||
.channels
|
||||
.into_iter()
|
||||
.map(|c| BridgeChannel {
|
||||
id: c.id.0,
|
||||
parent: c.parent.0,
|
||||
name: c.name,
|
||||
order: c.order,
|
||||
})
|
||||
.collect(),
|
||||
clients: s
|
||||
.clients
|
||||
.into_iter()
|
||||
.map(|c| BridgeClient {
|
||||
id: c.id.0,
|
||||
channel: c.channel.0,
|
||||
name: c.name,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Commands ----------
|
||||
|
||||
/// Connect to a TeamSpeak-compatible server and return the initial
|
||||
/// state snapshot. Honours the DEC-006 single-connection invariant
|
||||
/// via [`BridgeError::AlreadyConnected`].
|
||||
pub async fn connect(host: String, nickname: String) -> Result<BridgeSnapshot, BridgeError> {
|
||||
let cfg = chanora_core::ConnectConfig {
|
||||
address: host,
|
||||
nickname,
|
||||
password: None,
|
||||
identity: None,
|
||||
ready_timeout: Duration::from_secs(15),
|
||||
};
|
||||
let snap = runtime()
|
||||
.spawn(async move { session().connect(cfg).await })
|
||||
.await
|
||||
.map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
|
||||
Ok(snap.into())
|
||||
}
|
||||
|
||||
/// Re-fetch a fresh snapshot from the active connection.
|
||||
pub async fn snapshot() -> Result<BridgeSnapshot, BridgeError> {
|
||||
let snap = runtime()
|
||||
.spawn(async { session().snapshot().await })
|
||||
.await
|
||||
.map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
|
||||
Ok(snap.into())
|
||||
}
|
||||
|
||||
/// Disconnect from the server. No-op if not connected.
|
||||
pub async fn disconnect() -> Result<(), BridgeError> {
|
||||
runtime()
|
||||
.spawn(async { session().disconnect().await })
|
||||
.await
|
||||
.map_err(|e| BridgeError::Unmapped(format!("join: {e}")))??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// True if a connection is currently active.
|
||||
pub async fn is_connected() -> bool {
|
||||
runtime()
|
||||
.spawn(async { session().is_connected().await })
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
}
|
||||
@@ -0,0 +1,687 @@
|
||||
// This file is automatically generated, so please do not edit it.
|
||||
// @generated by `flutter_rust_bridge`@ 2.12.0.
|
||||
|
||||
#![allow(
|
||||
non_camel_case_types,
|
||||
unused,
|
||||
non_snake_case,
|
||||
clippy::needless_return,
|
||||
clippy::redundant_closure_call,
|
||||
clippy::redundant_closure,
|
||||
clippy::useless_conversion,
|
||||
clippy::unit_arg,
|
||||
clippy::unused_unit,
|
||||
clippy::double_parens,
|
||||
clippy::let_and_return,
|
||||
clippy::too_many_arguments,
|
||||
clippy::match_single_binding,
|
||||
clippy::clone_on_copy,
|
||||
clippy::let_unit_value,
|
||||
clippy::deref_addrof,
|
||||
clippy::explicit_auto_deref,
|
||||
clippy::borrow_deref_ref,
|
||||
clippy::uninlined_format_args,
|
||||
clippy::needless_borrow
|
||||
)]
|
||||
|
||||
// Section: imports
|
||||
|
||||
use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt};
|
||||
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
|
||||
use flutter_rust_bridge::{Handler, IntoIntoDart};
|
||||
|
||||
// Section: boilerplate
|
||||
|
||||
flutter_rust_bridge::frb_generated_boilerplate!(
|
||||
default_stream_sink_codec = SseCodec,
|
||||
default_rust_opaque = RustOpaqueMoi,
|
||||
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 = 978717843;
|
||||
|
||||
// Section: executor
|
||||
|
||||
flutter_rust_bridge::frb_generated_default_handler!();
|
||||
|
||||
// Section: wire_funcs
|
||||
|
||||
fn wire__crate__api__bridge_init_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
|
||||
flutter_rust_bridge::for_generated::TaskInfo {
|
||||
debug_name: "bridge_init",
|
||||
port: Some(port_),
|
||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||
},
|
||||
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);
|
||||
deserializer.end();
|
||||
move |context| {
|
||||
transform_result_sse::<_, ()>((move || {
|
||||
let output_ok = Result::<_, ()>::Ok({
|
||||
crate::api::bridge_init();
|
||||
})?;
|
||||
Ok(output_ok)
|
||||
})())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__connect_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||
flutter_rust_bridge::for_generated::TaskInfo {
|
||||
debug_name: "connect",
|
||||
port: Some(port_),
|
||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||
},
|
||||
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_host = <String>::sse_decode(&mut deserializer);
|
||||
let api_nickname = <String>::sse_decode(&mut deserializer);
|
||||
deserializer.end();
|
||||
move |context| async move {
|
||||
transform_result_sse::<_, crate::BridgeError>(
|
||||
(move || async move {
|
||||
let output_ok = crate::api::connect(api_host, api_nickname).await?;
|
||||
Ok(output_ok)
|
||||
})()
|
||||
.await,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__disconnect_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||
flutter_rust_bridge::for_generated::TaskInfo {
|
||||
debug_name: "disconnect",
|
||||
port: Some(port_),
|
||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||
},
|
||||
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);
|
||||
deserializer.end();
|
||||
move |context| async move {
|
||||
transform_result_sse::<_, crate::BridgeError>(
|
||||
(move || async move {
|
||||
let output_ok = crate::api::disconnect().await?;
|
||||
Ok(output_ok)
|
||||
})()
|
||||
.await,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__is_connected_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||
flutter_rust_bridge::for_generated::TaskInfo {
|
||||
debug_name: "is_connected",
|
||||
port: Some(port_),
|
||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||
},
|
||||
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);
|
||||
deserializer.end();
|
||||
move |context| async move {
|
||||
transform_result_sse::<_, ()>(
|
||||
(move || async move {
|
||||
let output_ok = Result::<_, ()>::Ok(crate::api::is_connected().await)?;
|
||||
Ok(output_ok)
|
||||
})()
|
||||
.await,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__snapshot_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||
flutter_rust_bridge::for_generated::TaskInfo {
|
||||
debug_name: "snapshot",
|
||||
port: Some(port_),
|
||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||
},
|
||||
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);
|
||||
deserializer.end();
|
||||
move |context| async move {
|
||||
transform_result_sse::<_, crate::BridgeError>(
|
||||
(move || async move {
|
||||
let output_ok = crate::api::snapshot().await?;
|
||||
Ok(output_ok)
|
||||
})()
|
||||
.await,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Section: dart2rust
|
||||
|
||||
impl SseDecode for String {
|
||||
// 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 = <Vec<u8>>::sse_decode(deserializer);
|
||||
return String::from_utf8(inner).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for bool {
|
||||
// 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_u8().unwrap() != 0
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for crate::api::BridgeChannel {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut var_id = <u64>::sse_decode(deserializer);
|
||||
let mut var_parent = <u64>::sse_decode(deserializer);
|
||||
let mut var_name = <String>::sse_decode(deserializer);
|
||||
let mut var_order = <i64>::sse_decode(deserializer);
|
||||
return crate::api::BridgeChannel {
|
||||
id: var_id,
|
||||
parent: var_parent,
|
||||
name: var_name,
|
||||
order: var_order,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for crate::api::BridgeClient {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut var_id = <u64>::sse_decode(deserializer);
|
||||
let mut var_channel = <u64>::sse_decode(deserializer);
|
||||
let mut var_name = <String>::sse_decode(deserializer);
|
||||
return crate::api::BridgeClient {
|
||||
id: var_id,
|
||||
channel: var_channel,
|
||||
name: var_name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for crate::BridgeError {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut tag_ = <i32>::sse_decode(deserializer);
|
||||
match tag_ {
|
||||
0 => {
|
||||
let mut var_field0 = <String>::sse_decode(deserializer);
|
||||
return crate::BridgeError::InvalidCommand(var_field0);
|
||||
}
|
||||
1 => {
|
||||
let mut var_field0 = <String>::sse_decode(deserializer);
|
||||
return crate::BridgeError::Connection(var_field0);
|
||||
}
|
||||
2 => {
|
||||
return crate::BridgeError::NotConnected;
|
||||
}
|
||||
3 => {
|
||||
return crate::BridgeError::AlreadyConnected;
|
||||
}
|
||||
4 => {
|
||||
let mut var_field0 = <String>::sse_decode(deserializer);
|
||||
return crate::BridgeError::Unmapped(var_field0);
|
||||
}
|
||||
_ => {
|
||||
unimplemented!("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let mut var_serverName = <String>::sse_decode(deserializer);
|
||||
let mut var_welcomeMessage = <String>::sse_decode(deserializer);
|
||||
let mut var_platform = <String>::sse_decode(deserializer);
|
||||
let mut var_version = <String>::sse_decode(deserializer);
|
||||
let mut var_channels = <Vec<crate::api::BridgeChannel>>::sse_decode(deserializer);
|
||||
let mut var_clients = <Vec<crate::api::BridgeClient>>::sse_decode(deserializer);
|
||||
return crate::api::BridgeSnapshot {
|
||||
server_name: var_serverName,
|
||||
welcome_message: var_welcomeMessage,
|
||||
platform: var_platform,
|
||||
version: var_version,
|
||||
channels: var_channels,
|
||||
clients: var_clients,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
deserializer.cursor.read_i64::<NativeEndian>().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for Vec<crate::api::BridgeChannel> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut len_ = <i32>::sse_decode(deserializer);
|
||||
let mut ans_ = Vec::with_capacity(len_ as usize);
|
||||
for idx_ in 0..len_ {
|
||||
ans_.push(<crate::api::BridgeChannel>::sse_decode(deserializer));
|
||||
}
|
||||
return ans_;
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for Vec<crate::api::BridgeClient> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut len_ = <i32>::sse_decode(deserializer);
|
||||
let mut ans_ = Vec::with_capacity(len_ as usize);
|
||||
for idx_ in 0..len_ {
|
||||
ans_.push(<crate::api::BridgeClient>::sse_decode(deserializer));
|
||||
}
|
||||
return ans_;
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for Vec<u8> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut len_ = <i32>::sse_decode(deserializer);
|
||||
let mut ans_ = Vec::with_capacity(len_ as usize);
|
||||
for idx_ in 0..len_ {
|
||||
ans_.push(<u8>::sse_decode(deserializer));
|
||||
}
|
||||
return ans_;
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for u64 {
|
||||
// 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_u64::<NativeEndian>().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for u8 {
|
||||
// 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_u8().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for () {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
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,
|
||||
ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len: i32,
|
||||
data_len: i32,
|
||||
) {
|
||||
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||
match func_id {
|
||||
1 => wire__crate__api__bridge_init_impl(port, ptr, rust_vec_len, data_len),
|
||||
2 => wire__crate__api__connect_impl(port, ptr, rust_vec_len, data_len),
|
||||
3 => wire__crate__api__disconnect_impl(port, ptr, rust_vec_len, data_len),
|
||||
4 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len),
|
||||
5 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn pde_ffi_dispatcher_sync_impl(
|
||||
func_id: i32,
|
||||
ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len: i32,
|
||||
data_len: i32,
|
||||
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
||||
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||
match func_id {
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
// Section: rust2dart
|
||||
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::api::BridgeChannel {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
[
|
||||
self.id.into_into_dart().into_dart(),
|
||||
self.parent.into_into_dart().into_dart(),
|
||||
self.name.into_into_dart().into_dart(),
|
||||
self.order.into_into_dart().into_dart(),
|
||||
]
|
||||
.into_dart()
|
||||
}
|
||||
}
|
||||
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::BridgeChannel {}
|
||||
impl flutter_rust_bridge::IntoIntoDart<crate::api::BridgeChannel> for crate::api::BridgeChannel {
|
||||
fn into_into_dart(self) -> crate::api::BridgeChannel {
|
||||
self
|
||||
}
|
||||
}
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::api::BridgeClient {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
[
|
||||
self.id.into_into_dart().into_dart(),
|
||||
self.channel.into_into_dart().into_dart(),
|
||||
self.name.into_into_dart().into_dart(),
|
||||
]
|
||||
.into_dart()
|
||||
}
|
||||
}
|
||||
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::BridgeClient {}
|
||||
impl flutter_rust_bridge::IntoIntoDart<crate::api::BridgeClient> for crate::api::BridgeClient {
|
||||
fn into_into_dart(self) -> crate::api::BridgeClient {
|
||||
self
|
||||
}
|
||||
}
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::BridgeError {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
match self {
|
||||
crate::BridgeError::InvalidCommand(field0) => {
|
||||
[0.into_dart(), field0.into_into_dart().into_dart()].into_dart()
|
||||
}
|
||||
crate::BridgeError::Connection(field0) => {
|
||||
[1.into_dart(), field0.into_into_dart().into_dart()].into_dart()
|
||||
}
|
||||
crate::BridgeError::NotConnected => [2.into_dart()].into_dart(),
|
||||
crate::BridgeError::AlreadyConnected => [3.into_dart()].into_dart(),
|
||||
crate::BridgeError::Unmapped(field0) => {
|
||||
[4.into_dart(), field0.into_into_dart().into_dart()].into_dart()
|
||||
}
|
||||
_ => {
|
||||
unimplemented!("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::BridgeError {}
|
||||
impl flutter_rust_bridge::IntoIntoDart<crate::BridgeError> for crate::BridgeError {
|
||||
fn into_into_dart(self) -> crate::BridgeError {
|
||||
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 {
|
||||
[
|
||||
self.server_name.into_into_dart().into_dart(),
|
||||
self.welcome_message.into_into_dart().into_dart(),
|
||||
self.platform.into_into_dart().into_dart(),
|
||||
self.version.into_into_dart().into_dart(),
|
||||
self.channels.into_into_dart().into_dart(),
|
||||
self.clients.into_into_dart().into_dart(),
|
||||
]
|
||||
.into_dart()
|
||||
}
|
||||
}
|
||||
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::BridgeSnapshot {}
|
||||
impl flutter_rust_bridge::IntoIntoDart<crate::api::BridgeSnapshot> for crate::api::BridgeSnapshot {
|
||||
fn into_into_dart(self) -> crate::api::BridgeSnapshot {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for String {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<Vec<u8>>::sse_encode(self.into_bytes(), serializer);
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for bool {
|
||||
// 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_u8(self as _).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for crate::api::BridgeChannel {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<u64>::sse_encode(self.id, serializer);
|
||||
<u64>::sse_encode(self.parent, serializer);
|
||||
<String>::sse_encode(self.name, serializer);
|
||||
<i64>::sse_encode(self.order, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for crate::api::BridgeClient {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<u64>::sse_encode(self.id, serializer);
|
||||
<u64>::sse_encode(self.channel, serializer);
|
||||
<String>::sse_encode(self.name, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for crate::BridgeError {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
match self {
|
||||
crate::BridgeError::InvalidCommand(field0) => {
|
||||
<i32>::sse_encode(0, serializer);
|
||||
<String>::sse_encode(field0, serializer);
|
||||
}
|
||||
crate::BridgeError::Connection(field0) => {
|
||||
<i32>::sse_encode(1, serializer);
|
||||
<String>::sse_encode(field0, serializer);
|
||||
}
|
||||
crate::BridgeError::NotConnected => {
|
||||
<i32>::sse_encode(2, serializer);
|
||||
}
|
||||
crate::BridgeError::AlreadyConnected => {
|
||||
<i32>::sse_encode(3, serializer);
|
||||
}
|
||||
crate::BridgeError::Unmapped(field0) => {
|
||||
<i32>::sse_encode(4, serializer);
|
||||
<String>::sse_encode(field0, serializer);
|
||||
}
|
||||
_ => {
|
||||
unimplemented!("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
<String>::sse_encode(self.server_name, serializer);
|
||||
<String>::sse_encode(self.welcome_message, serializer);
|
||||
<String>::sse_encode(self.platform, serializer);
|
||||
<String>::sse_encode(self.version, serializer);
|
||||
<Vec<crate::api::BridgeChannel>>::sse_encode(self.channels, serializer);
|
||||
<Vec<crate::api::BridgeClient>>::sse_encode(self.clients, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
serializer.cursor.write_i64::<NativeEndian>(self).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for Vec<crate::api::BridgeChannel> {
|
||||
// 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(self.len() as _, serializer);
|
||||
for item in self {
|
||||
<crate::api::BridgeChannel>::sse_encode(item, serializer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for Vec<crate::api::BridgeClient> {
|
||||
// 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(self.len() as _, serializer);
|
||||
for item in self {
|
||||
<crate::api::BridgeClient>::sse_encode(item, serializer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for Vec<u8> {
|
||||
// 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(self.len() as _, serializer);
|
||||
for item in self {
|
||||
<u8>::sse_encode(item, serializer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for u64 {
|
||||
// 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_u64::<NativeEndian>(self).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for u8 {
|
||||
// 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_u8(self).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for () {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
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.
|
||||
// @generated by `flutter_rust_bridge`@ 2.12.0.
|
||||
|
||||
// Section: imports
|
||||
|
||||
use super::*;
|
||||
use flutter_rust_bridge::for_generated::byteorder::{
|
||||
NativeEndian, ReadBytesExt, WriteBytesExt,
|
||||
};
|
||||
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
|
||||
use flutter_rust_bridge::{Handler, IntoIntoDart};
|
||||
|
||||
// Section: boilerplate
|
||||
|
||||
flutter_rust_bridge::frb_generated_boilerplate_io!();
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub use io::*;
|
||||
|
||||
/// cbindgen:ignore
|
||||
#[cfg(target_family = "wasm")]
|
||||
mod web {
|
||||
// This file is automatically generated, so please do not edit it.
|
||||
// @generated by `flutter_rust_bridge`@ 2.12.0.
|
||||
|
||||
// Section: imports
|
||||
|
||||
use super::*;
|
||||
use flutter_rust_bridge::for_generated::byteorder::{
|
||||
NativeEndian, ReadBytesExt, WriteBytesExt,
|
||||
};
|
||||
use flutter_rust_bridge::for_generated::wasm_bindgen;
|
||||
use flutter_rust_bridge::for_generated::wasm_bindgen::prelude::*;
|
||||
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
|
||||
use flutter_rust_bridge::{Handler, IntoIntoDart};
|
||||
|
||||
// Section: boilerplate
|
||||
|
||||
flutter_rust_bridge::frb_generated_boilerplate_web!();
|
||||
}
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub use web::*;
|
||||
@@ -1,42 +1,73 @@
|
||||
//! # `chanora_bridge`
|
||||
//!
|
||||
//! Typed Flutter/Rust bridge. Owns the canonical DTO catalogue for
|
||||
//! commands (Dart → Rust), results (Rust → Dart return values), and
|
||||
//! events (Rust → Dart streams).
|
||||
//! Typed Flutter/Rust bridge — schema-controlled DTOs for commands,
|
||||
//! results, and events. Backed by `flutter_rust_bridge` 2.x per
|
||||
//! DEC-014.
|
||||
//!
|
||||
//! Bridge tool per DEC-014: `flutter_rust_bridge` 2.x. The bridge
|
||||
//! integration glue (codegen invocations, the cdylib boundary) is
|
||||
//! added when `apps/chanora_flutter` is wired up; this crate owns
|
||||
//! only the type definitions and error mappings — explicitly *not*
|
||||
//! `tsclientlib` or raw subsystem types (SAD-067, SDD-079).
|
||||
//! ## Alpha scope
|
||||
//!
|
||||
//! ## Status
|
||||
//! Exposes three commands that target the Alpha release goal:
|
||||
//!
|
||||
//! Scaffold only.
|
||||
//! * `bridge_init()` — one-time process initialisation. Sets up
|
||||
//! logging.
|
||||
//! * `connect(host, nickname)` — connects to a TS3-compatible
|
||||
//! server and returns a typed [`ConnectResultDto`] containing the
|
||||
//! server snapshot.
|
||||
//! * `disconnect()` — clean disconnect.
|
||||
//! * `snapshot()` — re-fetch the current server snapshot.
|
||||
//!
|
||||
//! No audio commands cross the bridge in Alpha; audio is Beta scope.
|
||||
//!
|
||||
//! ## Boundary discipline
|
||||
//!
|
||||
//! Every type in this module is `Serialize + Deserialize` over owned
|
||||
//! primitives or `String`s. No `tsclientlib`, `cpal`, or backend
|
||||
//! types may appear in the public surface (SAD-067, SDD-079).
|
||||
//!
|
||||
//! Note: this crate cannot use `#![forbid(unsafe_code)]` because the
|
||||
//! FRB-generated glue (in `frb_generated`) legitimately uses unsafe
|
||||
//! for the FFI boundary. Hand-written code in this crate must
|
||||
//! nonetheless avoid `unsafe` and is held to that standard by review.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub mod api;
|
||||
mod frb_generated;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors raised at the bridge boundary. Production code must keep
|
||||
/// `BridgeError` user-safe — no secrets, no protocol details, no
|
||||
/// path information beyond what the redaction policy permits.
|
||||
#[derive(Debug, Error, Clone, Serialize, Deserialize)]
|
||||
/// these user-safe — no secrets, no protocol details, no path
|
||||
/// information beyond what the redaction policy permits.
|
||||
#[derive(Debug, Error, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum BridgeError {
|
||||
/// The caller submitted a malformed command DTO.
|
||||
#[error("invalid command: {0}")]
|
||||
InvalidCommand(String),
|
||||
/// An unknown / unmapped error escaped the subsystem boundary.
|
||||
/// Production callers should never see this; if it appears it is
|
||||
/// a mapping bug in this crate.
|
||||
/// Connection layer failure (typed-mapped from CoreError).
|
||||
#[error("connection: {0}")]
|
||||
Connection(String),
|
||||
/// Operation requires an active connection.
|
||||
#[error("not connected")]
|
||||
NotConnected,
|
||||
/// Already connected; DEC-006 forbids a second concurrent
|
||||
/// connection.
|
||||
#[error("already connected")]
|
||||
AlreadyConnected,
|
||||
/// An unmapped error escaped the subsystem boundary. Production
|
||||
/// callers should never see this; if they do, it is a mapping
|
||||
/// bug here.
|
||||
#[error("unmapped: {0}")]
|
||||
Unmapped(String),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_compiles() {}
|
||||
impl From<chanora_core::CoreError> for BridgeError {
|
||||
fn from(e: chanora_core::CoreError) -> Self {
|
||||
match e {
|
||||
chanora_core::CoreError::NotConnected => BridgeError::NotConnected,
|
||||
chanora_core::CoreError::AlreadyConnected => BridgeError::AlreadyConnected,
|
||||
chanora_core::CoreError::Protocol(p) => BridgeError::Connection(format!("{p}")),
|
||||
other => BridgeError::Unmapped(format!("{other}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user