P4: messages, topics, pubkey
Backend
- api/messages.rs covers send/pop/reply/status with an externally
tagged MessageDestination enum that matches the daemon's
{ip|pk: ...} body shape; pop_message uses an inflated request
timeout to outlast the long-poll window
- api/topics.rs implements default action, topic CRUD, sources
whitelist, and forward-socket get/set/remove. POST /topics ships
the raw base64 string as the body (not JSON); path segments are
percent-encoded inline (topics contain '/' and '+')
- api/pubkey.rs resolves an overlay IPv6 to a hex public key
- poller spawns a third long-poll loop on /messages?peek=false
that fans every inbound message into a 200-deep ring buffer and
emits messages://incoming for the UI
Frontend
- messages store: live inbox via the event, persisted outbox via
tauri-plugin-store keyed under outbox.json
- ComposeMessage form: ip/pk toggle, optional UTF-8 topic and
payload that get base64-encoded with a TextEncoder-based helper
- MessageList renders printable payloads decoded; binary payloads
fall back to a "(N bytes binary)" hint
- Topics view: split layout with whitelist on the left, per-topic
sources/forward editor on the right; default-action toggle is
surfaced at the top
This commit is contained in:
+62
-8
@@ -1,6 +1,8 @@
|
||||
use crate::api::messages::IncomingMessage;
|
||||
use crate::api::peers;
|
||||
use crate::sidecar::SidecarHandle;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
@@ -9,10 +11,15 @@ use tracing::warn;
|
||||
|
||||
const PEERS_INTERVAL: Duration = Duration::from_secs(3);
|
||||
const ROUTES_INTERVAL: Duration = Duration::from_secs(5);
|
||||
const INBOX_LONG_POLL_SECS: u64 = 30;
|
||||
const INBOX_RETRY_BACKOFF: Duration = Duration::from_secs(2);
|
||||
const INBOX_CAPACITY: usize = 200;
|
||||
|
||||
pub struct Poller {
|
||||
peers_handle: Mutex<Option<JoinHandle<()>>>,
|
||||
routes_handle: Mutex<Option<JoinHandle<()>>>,
|
||||
inbox_handle: Mutex<Option<JoinHandle<()>>>,
|
||||
inbox: Mutex<VecDeque<IncomingMessage>>,
|
||||
}
|
||||
|
||||
impl Poller {
|
||||
@@ -20,30 +27,49 @@ impl Poller {
|
||||
Arc::new(Self {
|
||||
peers_handle: Mutex::new(None),
|
||||
routes_handle: Mutex::new(None),
|
||||
inbox_handle: Mutex::new(None),
|
||||
inbox: Mutex::new(VecDeque::with_capacity(INBOX_CAPACITY)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Spawn the two background loops. Cancels any previously-running tasks
|
||||
/// so consecutive `start_daemon` calls don't leak handles.
|
||||
pub fn start(self: &Arc<Self>, app: AppHandle, sidecar: Arc<SidecarHandle>) {
|
||||
self.stop();
|
||||
*self.peers_handle.lock() = Some(spawn_peers_loop(app.clone(), Arc::clone(&sidecar)));
|
||||
*self.routes_handle.lock() = Some(spawn_routes_loop(app, sidecar));
|
||||
*self.routes_handle.lock() = Some(spawn_routes_loop(app.clone(), Arc::clone(&sidecar)));
|
||||
*self.inbox_handle.lock() = Some(spawn_inbox_loop(
|
||||
app,
|
||||
Arc::clone(&sidecar),
|
||||
Arc::clone(self),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
if let Some(h) = self.peers_handle.lock().take() {
|
||||
h.abort();
|
||||
for slot in [&self.peers_handle, &self.routes_handle, &self.inbox_handle] {
|
||||
if let Some(h) = slot.lock().take() {
|
||||
h.abort();
|
||||
}
|
||||
}
|
||||
if let Some(h) = self.routes_handle.lock().take() {
|
||||
h.abort();
|
||||
}
|
||||
|
||||
pub fn inbox_snapshot(&self) -> Vec<IncomingMessage> {
|
||||
self.inbox.lock().iter().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn clear_inbox(&self) {
|
||||
self.inbox.lock().clear();
|
||||
}
|
||||
|
||||
fn push_inbox(&self, msg: IncomingMessage) {
|
||||
let mut buf = self.inbox.lock();
|
||||
if buf.len() >= INBOX_CAPACITY {
|
||||
buf.pop_front();
|
||||
}
|
||||
buf.push_back(msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_peers_loop(app: AppHandle, sidecar: Arc<SidecarHandle>) -> JoinHandle<()> {
|
||||
tokio::spawn(async move {
|
||||
// Tick once immediately so the UI doesn't wait the full interval.
|
||||
let mut first = true;
|
||||
loop {
|
||||
if !first {
|
||||
@@ -87,3 +113,31 @@ fn spawn_routes_loop(app: AppHandle, sidecar: Arc<SidecarHandle>) -> JoinHandle<
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn spawn_inbox_loop(
|
||||
app: AppHandle,
|
||||
sidecar: Arc<SidecarHandle>,
|
||||
me: Arc<Poller>,
|
||||
) -> JoinHandle<()> {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let Some(client) = sidecar.client() else {
|
||||
break;
|
||||
};
|
||||
// Each iteration is a fresh long-poll. The daemon answers as
|
||||
// soon as a message arrives, or returns an empty body / 204
|
||||
// when the timeout window elapses.
|
||||
match client.pop_message(false, INBOX_LONG_POLL_SECS, None).await {
|
||||
Ok(Some(msg)) => {
|
||||
me.push_inbox(msg.clone());
|
||||
let _ = app.emit("messages://incoming", &msg);
|
||||
}
|
||||
Ok(None) => {} // window expired, loop
|
||||
Err(e) => {
|
||||
warn!(error = %e, "inbox: pop_message failed");
|
||||
tokio::time::sleep(INBOX_RETRY_BACKOFF).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user