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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user