//! Client event system — real-time notifications from the SDK. /// Events emitted by the SDK to the UI layer. #[derive(Debug, Clone)] pub enum ClientEvent { /// Successfully connected to the server. Connected, /// Disconnected from the server. Disconnected { reason: String }, /// Connection lost, attempting to reconnect. Reconnecting { attempt: u32 }, /// Registration succeeded. Registered { username: String }, /// Login succeeded. LoggedIn { username: String }, /// Logged out. LoggedOut { username: String }, /// Authentication succeeded. Authenticated { username: String }, /// A new message was received in a conversation. MessageReceived { conversation_id: [u8; 16], sender_key: Vec, sender_name: Option, body: String, timestamp_ms: u64, }, /// A message was sent successfully. MessageSent { conversation_id: [u8; 16], seq: u64, }, /// A new conversation was created or discovered. ConversationCreated { conversation_id: [u8; 16], display_name: String, }, /// A member was added to a group conversation. MemberAdded { conversation_id: [u8; 16], member_key: Vec, }, /// A member was removed from a group conversation. MemberRemoved { conversation_id: [u8; 16], member_key: Vec, }, /// Server-push event received. PushEvent { event_type: u16, payload: Vec, }, /// A message was queued in the offline outbox (send failed or disconnected). MessageQueued { outbox_id: i64, conversation_id: [u8; 16], }, /// Outbox flush completed after reconnect. OutboxFlushed { sent: usize, failed: usize, }, /// Gap detected in message sequence numbers. MessageGap { conversation_id: [u8; 16], expected_seq: u64, received_seq: u64, }, /// An error occurred in the background. Error { message: String }, } #[cfg(test)] mod tests { use super::*; use tokio::sync::broadcast; #[test] fn event_broadcast_send_receive() { let (tx, mut rx) = broadcast::channel::(16); tx.send(ClientEvent::Connected).unwrap(); tx.send(ClientEvent::Disconnected { reason: "test".into(), }) .unwrap(); let e1 = rx.try_recv().unwrap(); assert!(matches!(e1, ClientEvent::Connected)); let e2 = rx.try_recv().unwrap(); assert!(matches!(e2, ClientEvent::Disconnected { reason } if reason == "test")); } #[test] fn event_broadcast_multiple_subscribers() { let (tx, mut rx1) = broadcast::channel::(16); let mut rx2 = tx.subscribe(); tx.send(ClientEvent::Registered { username: "alice".into(), }) .unwrap(); let e1 = rx1.try_recv().unwrap(); let e2 = rx2.try_recv().unwrap(); assert!(matches!(e1, ClientEvent::Registered { username } if username == "alice")); assert!(matches!(e2, ClientEvent::Registered { username } if username == "alice")); } #[test] fn event_no_subscribers_does_not_panic() { let (tx, _) = broadcast::channel::(16); // Send with no active receiver — should return Err but not panic. let result = tx.send(ClientEvent::Connected); assert!(result.is_err()); // no receivers } #[test] fn event_clone_preserves_data() { let event = ClientEvent::MessageReceived { conversation_id: [1u8; 16], sender_key: vec![2u8; 32], sender_name: Some("bob".into()), body: "hello world".into(), timestamp_ms: 12345, }; let cloned = event.clone(); match cloned { ClientEvent::MessageReceived { conversation_id, sender_key, sender_name, body, timestamp_ms, } => { assert_eq!(conversation_id, [1u8; 16]); assert_eq!(sender_key, vec![2u8; 32]); assert_eq!(sender_name, Some("bob".to_string())); assert_eq!(body, "hello world"); assert_eq!(timestamp_ms, 12345); } _ => panic!("wrong variant after clone"), } } #[test] fn event_debug_format() { let event = ClientEvent::Error { message: "something went wrong".into(), }; let dbg = format!("{event:?}"); assert!(dbg.contains("something went wrong")); } #[test] fn all_event_variants_are_clone() { // Verify all variants can be cloned without issue. let events: Vec = vec![ ClientEvent::Connected, ClientEvent::Disconnected { reason: "r".into() }, ClientEvent::Reconnecting { attempt: 1 }, ClientEvent::Registered { username: "u".into() }, ClientEvent::LoggedIn { username: "u".into() }, ClientEvent::LoggedOut { username: "u".into() }, ClientEvent::Authenticated { username: "u".into() }, ClientEvent::MessageReceived { conversation_id: [0; 16], sender_key: vec![], sender_name: None, body: "b".into(), timestamp_ms: 0, }, ClientEvent::MessageSent { conversation_id: [0; 16], seq: 0, }, ClientEvent::ConversationCreated { conversation_id: [0; 16], display_name: "d".into(), }, ClientEvent::MemberAdded { conversation_id: [0; 16], member_key: vec![], }, ClientEvent::MemberRemoved { conversation_id: [0; 16], member_key: vec![], }, ClientEvent::PushEvent { event_type: 0, payload: vec![], }, ClientEvent::MessageQueued { outbox_id: 0, conversation_id: [0; 16], }, ClientEvent::OutboxFlushed { sent: 0, failed: 0 }, ClientEvent::MessageGap { conversation_id: [0; 16], expected_seq: 0, received_seq: 1, }, ClientEvent::Error { message: "e".into() }, ]; for event in &events { let _ = event.clone(); } assert_eq!(events.len(), 17); } }