chore: restore product scaffold to rollback baseline

This commit is contained in:
Edison Jwa
2026-05-29 14:02:04 +09:00
parent 2896f14ec9
commit fe6e07353e
434 changed files with 27278 additions and 63230 deletions
+2
View File
@@ -15,6 +15,7 @@ chanora_state = { path = "../../crates/chanora_state" }
chanora_audio = { path = "../../crates/chanora_audio" }
chanora_storage = { path = "../../crates/chanora_storage" }
chanora_diagnostics = { path = "../../crates/chanora_diagnostics" }
chanora_server_prefetch = { path = "../../crates/chanora_server_prefetch" }
thiserror.workspace = true
tracing.workspace = true
tokio = { version = "1", features = ["sync", "rt", "macros"] }
@@ -22,5 +23,6 @@ tokio = { version = "1", features = ["sync", "rt", "macros"] }
[dev-dependencies]
# Used by integration tests to inspect the bookmark DB row layout
# without going through the public repository API.
chanora_server_prefetch = { path = "../../crates/chanora_server_prefetch", features = ["test-support"] }
rusqlite = { version = "0.32", features = ["bundled"] }
tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] }
+182 -9
View File
@@ -46,6 +46,7 @@ use tokio::sync::{broadcast, oneshot, watch, Mutex};
use tokio::task::JoinHandle;
use tracing::{info, warn};
use chanora_server_prefetch::ServerPrefetcher;
use chanora_state::channel_join::{
self, AuthoritativeSource, ChannelId as JoinChannelId, ChannelJoinEvent, ChannelJoinState,
ConnectionEpoch, JoinFailureKind,
@@ -64,8 +65,8 @@ pub use chanora_diagnostics::{
RedactingLogLayer, Redactor, DEFAULT_LOG_CAPACITY,
};
pub use chanora_protocol::{
ChannelInfo, ChatMessage, ClientInfo, ConnectConfig, DisconnectReason, MessageTarget,
ProtocolError, ServerActivity, ServerSnapshot,
ChannelInfo, ChatMessage, ClientInfo, ClientProfile, ConnectConfig, DisconnectReason,
MessageTarget, ProtocolError, ServerActivity, ServerSnapshot,
};
pub use chanora_storage::{Bookmark, BookmarkRepository, IdentityFileStore};
@@ -431,6 +432,9 @@ pub struct ChanoraSession {
/// extension). Lives alongside the identity file. Wired by
/// [`Self::init_storage`].
bookmark_store: Arc<Mutex<Option<BookmarkRepository>>>,
/// Invisible server-address prefetch cache. Warmed by Flutter typing
/// but validated by Rust before Connect can reuse it.
server_prefetch: ServerPrefetcher,
/// Single transmit-mode selector for the whole session
/// lifetime (SAD-083). Re-wired to a fresh
/// [`AudioTransmitGate`] every time the audio engine starts;
@@ -483,6 +487,7 @@ impl ChanoraSession {
network_tx,
identity_store: Arc::new(Mutex::new(None)),
bookmark_store: Arc::new(Mutex::new(None)),
server_prefetch: ServerPrefetcher::new(),
voice_selector: selector,
release_tail,
pending_binding: Arc::new(Mutex::new(None)),
@@ -628,6 +633,38 @@ impl ChanoraSession {
self.events_tx.subscribe()
}
/// Warm server-address resolution for a later user-initiated connect.
pub async fn prefetch_server(&self, host: String) -> Result<(), CoreError> {
let host = host.trim().to_lowercase();
if let Err(err) = self.server_prefetch.prefetch(host.clone()).await {
warn!(
target: "chanora_core",
host = %host,
error = %err,
"server prefetch setup failed"
);
}
Ok(())
}
async fn apply_prefetched_resolution(&self, cfg: &mut ConnectConfig) {
cfg.resolved_address = None;
let host = cfg.address.trim();
if host.is_empty() {
return;
}
if let Some(addr) = self.server_prefetch.fresh_match(host).await {
cfg.resolved_address = Some(addr);
}
}
async fn prepare_connect_configs(&self, cfg: ConnectConfig) -> (ConnectConfig, ConnectConfig) {
let stored_cfg = connect_config_without_prefetched_resolution(&cfg);
let mut dial_cfg = stored_cfg.clone();
self.apply_prefetched_resolution(&mut dial_cfg).await;
(stored_cfg, dial_cfg)
}
/// Connect to a server. Fails with [`CoreError::AlreadyConnected`]
/// if a connection is already active (DEC-006). Audio is not
/// started automatically; call [`Self::start_audio`] after.
@@ -679,7 +716,9 @@ impl ChanoraSession {
}
}
let client = chanora_protocol::ProtocolClient::connect(cfg.clone()).await?;
let (cfg, dial_cfg) = self.prepare_connect_configs(cfg).await;
let client = chanora_protocol::ProtocolClient::connect(dial_cfg).await?;
let snap = client.snapshot().await?;
let epoch = self.allocate_connection_epoch().await;
let mut join_state = ChannelJoinState::new(epoch);
@@ -896,6 +935,13 @@ impl ChanoraSession {
Ok(snap)
}
/// Fetch richer profile and live connection details for one online client.
pub async fn client_profile(&self, client_id: u64) -> Result<ClientProfile, CoreError> {
let guard = self.inner.lock().await;
let state = guard.as_ref().ok_or(CoreError::NotConnected)?;
Ok(state.protocol.client_profile(client_id).await?)
}
/// True if a connection is currently active.
pub async fn is_connected(&self) -> bool {
self.inner.lock().await.is_some()
@@ -928,6 +974,12 @@ impl ChanoraSession {
rec.drain()
}
/// Snapshot recorded protocol events without clearing them.
pub async fn protocol_events_snapshot(&self) -> Vec<String> {
let rec = self.event_recorder.lock().await;
rec.snapshot()
}
/// Record a platform lifecycle event (SRS-138).
pub async fn record_lifecycle_event(&self, state: &str) {
let mut rec = self.event_recorder.lock().await;
@@ -1234,11 +1286,11 @@ impl ChanoraSession {
audio.set_output_muted(muted);
}
}
// Muting the speaker is treated as a local deafen: it must
// also clamp microphone TX so users cannot keep talking while
// unable to hear replies. This does not send server-side
// input-mute; it only drives the local transmit gate.
let mic_disabled = state.local_input_muted || state.local_output_muted;
// Input mute must also stop local outbound voice production
// so the transmit selector stays in sync with the server-side
// mic mute. Output mute is playback-only and must not affect
// the mic gate.
let mic_disabled = state.local_input_muted;
self.voice_selector.set_hard_mute(mic_disabled);
Ok(())
}
@@ -1323,6 +1375,19 @@ impl ChanoraSession {
Ok(audio.audio_processing_stats())
}
/// Best-effort audio-processing diagnostics for user exports.
///
/// This intentionally never waits for the connection mutex. During
/// server join the connect path owns that mutex while awaiting network
/// I/O; diagnostics must stay responsive and omit live audio stats
/// rather than blocking the UI thread.
pub fn audio_processing_stats_if_ready(&self) -> Option<AudioProcessingStats> {
let guard = self.inner.try_lock().ok()?;
let state = guard.as_ref()?;
let audio = state.audio.as_ref()?;
Some(audio.audio_processing_stats())
}
/// Configure the preferred Silero ONNX VAD model path.
///
/// This does not require an active connection. Running iOS audio
@@ -2367,6 +2432,12 @@ fn map_join_error_code(code: channel_join::JoinErrorCode) -> VoiceJoinErrorCode
}
}
fn connect_config_without_prefetched_resolution(cfg: &ConnectConfig) -> ConnectConfig {
let mut cfg = cfg.clone();
cfg.resolved_address = None;
cfg
}
#[cfg(test)]
mod tests {
use super::*;
@@ -2376,6 +2447,109 @@ mod tests {
let _ = ChanoraSession::new();
}
#[test]
fn audio_stats_nonblocking_under_lock() {
let s = ChanoraSession::new();
let _guard = s.inner.try_lock().unwrap();
assert!(s.audio_processing_stats_if_ready().is_none());
}
#[test]
fn audio_stats_absent_when_disconnected() {
let s = ChanoraSession::new();
assert!(s.audio_processing_stats_if_ready().is_none());
}
#[tokio::test]
async fn connect_uses_fresh_prefetch() {
let session = ChanoraSession::new();
let addr: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap();
let generation = session.server_prefetch.begin_for_test("example.com").await;
session
.server_prefetch
.store_success_for_test(generation, "example.com", addr, std::time::Instant::now())
.await;
let mut cfg = ConnectConfig::default();
cfg.address = " example.com ".to_string();
session.apply_prefetched_resolution(&mut cfg).await;
assert_eq!(cfg.resolved_address, Some(addr));
}
#[tokio::test]
async fn connect_clears_untrusted_resolved_address() {
let session = ChanoraSession::new();
let mut cfg = ConnectConfig::default();
cfg.address = "example.com".to_string();
cfg.resolved_address = Some("127.0.0.1:9987".parse().unwrap());
session.apply_prefetched_resolution(&mut cfg).await;
assert_eq!(cfg.resolved_address, None);
}
#[tokio::test]
async fn prefetch_swallows_setup_errors() {
let session = ChanoraSession::new();
session
.server_prefetch
.fail_next_prefetch_setup_for_test("resolver unavailable")
.await;
let result = session
.prefetch_server(" Example.COM ".to_string())
.await;
assert!(result.is_ok());
}
#[test]
fn sanitize_config_clears_resolved_address() {
let addr: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap();
let mut cfg = ConnectConfig::default();
cfg.address = "example.com".to_string();
cfg.nickname = "tester".to_string();
cfg.password = Some("secret".to_string());
cfg.identity = Some("identity".to_string());
cfg.ready_timeout = std::time::Duration::from_secs(42);
cfg.resolved_address = Some(addr);
let cleaned = connect_config_without_prefetched_resolution(&cfg);
assert_eq!(cleaned.address, cfg.address);
assert_eq!(cleaned.nickname, cfg.nickname);
assert_eq!(cleaned.password, cfg.password);
assert_eq!(cleaned.identity, cfg.identity);
assert_eq!(cleaned.ready_timeout, cfg.ready_timeout);
assert_eq!(cleaned.resolved_address, None);
}
#[tokio::test]
async fn prepare_configs_sanitizes_stored_keeps_dial() {
let session = ChanoraSession::new();
let cached: std::net::SocketAddr = "127.0.0.1:9987".parse().unwrap();
let untrusted: std::net::SocketAddr = "127.0.0.2:9987".parse().unwrap();
let generation = session.server_prefetch.begin_for_test("example.com").await;
session
.server_prefetch
.store_success_for_test(generation, "example.com", cached, std::time::Instant::now())
.await;
let mut cfg = ConnectConfig::default();
cfg.address = "example.com".to_string();
cfg.resolved_address = Some(untrusted);
let (stored_cfg, dial_cfg) = session.prepare_connect_configs(cfg).await;
assert_eq!(stored_cfg.resolved_address, None);
assert_eq!(dial_cfg.resolved_address, Some(cached));
}
#[tokio::test]
async fn disconnect_when_not_connected_is_noop() {
let s = ChanoraSession::new();
@@ -2411,7 +2585,6 @@ mod tests {
],
clients: vec![ClientInfo {
id: chanora_protocol::ClientId(10),
uid: "uid".into(),
channel: chanora_protocol::ChannelId(1),
name: "u".into(),
input_muted: false,
+1
View File
@@ -18,6 +18,7 @@ async fn alpha_smoke() {
password: None,
identity: None,
ready_timeout: Duration::from_secs(15),
resolved_address: None,
};
let snap = s.connect(cfg).await.expect("connect");
println!(