v0.0.5: WebSocket real-time messaging

Server:
- WS endpoint: /v1/ws/:fingerprint
- Connection registry in AppState (fingerprint → WS senders)
- On connect: flushes queued DB messages, then pushes in real-time
- send_message: pushes to WS if connected, falls back to DB queue
- Auto-cleanup on disconnect
- WS accepts both binary and JSON text frames for sending

Web client:
- Replaces 2-second HTTP polling with persistent WebSocket
- Auto-reconnects on disconnect (3-second backoff)
- Sends via WS when connected, HTTP fallback
- Messages arrive instantly (no polling delay)
- "Real-time connection established" shown on connect

HTTP polling still works:
- CLI recv command uses HTTP (unchanged)
- Web falls back to HTTP if WS fails
- Mules/scripts can still use HTTP API

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-27 09:41:50 +04:00
parent 6cf2a1814c
commit 2ca25fd2bf
8 changed files with 425 additions and 99 deletions

View File

@@ -1,15 +1,65 @@
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{broadcast, Mutex, mpsc};
use crate::db::Database;
/// Per-connection sender: messages are pushed here for instant delivery.
pub type WsSender = mpsc::UnboundedSender<Vec<u8>>;
/// Connected clients: fingerprint → list of WS senders (multiple devices).
pub type Connections = Arc<Mutex<HashMap<String, Vec<WsSender>>>>;
#[derive(Clone)]
pub struct AppState {
pub db: Arc<Database>,
pub connections: Connections,
}
impl AppState {
pub fn new(data_dir: &str) -> anyhow::Result<Self> {
let db = Database::open(data_dir)?;
Ok(AppState { db: Arc::new(db) })
Ok(AppState {
db: Arc::new(db),
connections: Arc::new(Mutex::new(HashMap::new())),
})
}
/// Try to push a message to a connected client. Returns true if delivered.
pub async fn push_to_client(&self, fingerprint: &str, message: &[u8]) -> bool {
let conns = self.connections.lock().await;
if let Some(senders) = conns.get(fingerprint) {
let mut delivered = false;
for sender in senders {
if sender.send(message.to_vec()).is_ok() {
delivered = true;
}
}
delivered
} else {
false
}
}
/// Register a WS connection for a fingerprint.
pub async fn register_ws(&self, fingerprint: &str) -> mpsc::UnboundedReceiver<Vec<u8>> {
let (tx, rx) = mpsc::unbounded_channel();
let mut conns = self.connections.lock().await;
conns.entry(fingerprint.to_string()).or_default().push(tx);
tracing::info!("WS registered for {} ({} total connections)", fingerprint,
conns.values().map(|v| v.len()).sum::<usize>());
rx
}
/// Unregister a WS connection.
pub async fn unregister_ws(&self, fingerprint: &str, sender: &WsSender) {
let mut conns = self.connections.lock().await;
if let Some(senders) = conns.get_mut(fingerprint) {
senders.retain(|s| !s.same_channel(sender));
if senders.is_empty() {
conns.remove(fingerprint);
}
}
tracing::info!("WS unregistered for {}", fingerprint);
}
}