mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-08-25 23:21:10 +00:00
feat(client): add threaded transport polling (#125)
The client, not the app, now drives the transport; events are delivered asynchronously, per ADR 0001. - ChatClient owns Arc<Mutex<Core>> + a worker thread. - The worker select!s over the inbound and shutdown channels; Drop joins it. Outbound runs on the caller's thread. - A single Transport (DeliveryService + inbound()) owns both directions of the boundary, so the client takes one transport rather than a (delivery, inbound) pair. InProcessDelivery::new, CDelivery, and chat-cli's transports implement it. - FFI replaces client_receive with client_push_inbound + client_poll_events. - chat-cli drains Receiver<Event>; inbound and event channels are both crossbeam. - Corrects ADR 0001's inbound sequence to push — the worker parks on select!, it never polls.
This commit is contained in:
+20
-23
@@ -1,10 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc;
|
||||
|
||||
use anyhow::Result;
|
||||
use arboard::Clipboard;
|
||||
use crossbeam_channel::Receiver;
|
||||
use logos_chat::{ChatClient, DeliveryService, EphemeralRegistry, Event, RegistrationService};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -41,9 +41,9 @@ pub struct AppState {
|
||||
pub active_chat: Option<String>,
|
||||
}
|
||||
|
||||
pub struct ChatApp<D: DeliveryService, R: RegistrationService = EphemeralRegistry> {
|
||||
pub client: ChatClient<D, R>,
|
||||
inbound: mpsc::Receiver<Vec<u8>>,
|
||||
pub struct ChatApp<T: DeliveryService, R: RegistrationService = EphemeralRegistry> {
|
||||
pub client: ChatClient<T, R>,
|
||||
events: Receiver<Event>,
|
||||
pub state: AppState,
|
||||
/// Ephemeral command output — not persisted, cleared on chat switch.
|
||||
command_output: Vec<DisplayMessage>,
|
||||
@@ -53,14 +53,14 @@ pub struct ChatApp<D: DeliveryService, R: RegistrationService = EphemeralRegistr
|
||||
state_path: PathBuf,
|
||||
}
|
||||
|
||||
impl<D, R> ChatApp<D, R>
|
||||
impl<T, R> ChatApp<T, R>
|
||||
where
|
||||
D: DeliveryService + 'static,
|
||||
R: RegistrationService + 'static,
|
||||
T: DeliveryService + Send + 'static,
|
||||
R: RegistrationService + Send + 'static,
|
||||
{
|
||||
pub fn new(
|
||||
client: ChatClient<D, R>,
|
||||
inbound: mpsc::Receiver<Vec<u8>>,
|
||||
client: ChatClient<T, R>,
|
||||
events: Receiver<Event>,
|
||||
user_name: &str,
|
||||
data_dir: &Path,
|
||||
) -> Result<Self> {
|
||||
@@ -80,7 +80,7 @@ where
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
inbound,
|
||||
events,
|
||||
state,
|
||||
command_output: Vec::new(),
|
||||
input: String::new(),
|
||||
@@ -146,19 +146,13 @@ where
|
||||
}
|
||||
|
||||
pub fn process_incoming(&mut self) -> Result<()> {
|
||||
while let Ok(payload) = self.inbound.try_recv() {
|
||||
match self.client.receive(&payload) {
|
||||
Ok(events) => {
|
||||
for event in events {
|
||||
self.handle_event(event);
|
||||
}
|
||||
self.save_state()?;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("receive error: {e:?}");
|
||||
self.status = format!("Could not decrypt incoming message: {e}");
|
||||
}
|
||||
}
|
||||
let mut received = false;
|
||||
while let Ok(event) = self.events.try_recv() {
|
||||
self.handle_event(event);
|
||||
received = true;
|
||||
}
|
||||
if received {
|
||||
self.save_state()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -195,6 +189,9 @@ where
|
||||
timestamp: now(),
|
||||
});
|
||||
}
|
||||
Event::InboundError { message } => {
|
||||
self.status = format!("Could not process incoming message: {message}");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-31
@@ -4,11 +4,13 @@ mod ui;
|
||||
mod utils;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use logos_chat::{ChatClient, DeliveryService, HttpRegistry, RegistrationService, StorageConfig};
|
||||
use crossbeam_channel::Receiver;
|
||||
use logos_chat::{
|
||||
ChatClient, DeliveryService, Event, HttpRegistry, RegistrationService, StorageConfig, Transport,
|
||||
};
|
||||
|
||||
use app::ChatApp;
|
||||
|
||||
@@ -72,9 +74,9 @@ fn main() -> Result<()> {
|
||||
match cli.transport {
|
||||
TransportKind::File => {
|
||||
let transport_dir = cli.data.join("transport");
|
||||
let (transport, inbound) = transport::file::FileTransport::new(&transport_dir)
|
||||
let transport = transport::file::FileTransport::new(&transport_dir)
|
||||
.context("failed to create file transport")?;
|
||||
run(transport, inbound, &cli)
|
||||
run(transport, &cli)
|
||||
}
|
||||
#[cfg(logos_delivery)]
|
||||
TransportKind::LogosDelivery => {
|
||||
@@ -88,20 +90,15 @@ fn main() -> Result<()> {
|
||||
tcp_port: cli.port,
|
||||
..Default::default()
|
||||
};
|
||||
let (transport, inbound) =
|
||||
Service::start(cfg).context("failed to start logos-delivery")?;
|
||||
let transport = Service::start(cfg).context("failed to start logos-delivery")?;
|
||||
|
||||
println!("Node connected. Initializing chat client...");
|
||||
run(transport, inbound, &cli)
|
||||
run(transport, &cli)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run<D: DeliveryService + 'static>(
|
||||
transport: D,
|
||||
inbound: mpsc::Receiver<Vec<u8>>,
|
||||
cli: &Cli,
|
||||
) -> Result<()> {
|
||||
fn run<T: Transport>(transport: T, cli: &Cli) -> Result<()> {
|
||||
let db_path = cli
|
||||
.db
|
||||
.clone()
|
||||
@@ -118,31 +115,27 @@ fn run<D: DeliveryService + 'static>(
|
||||
match cli.registry_url.as_deref() {
|
||||
Some(url) => {
|
||||
let registry = HttpRegistry::new(url);
|
||||
let client =
|
||||
let (client, events) =
|
||||
ChatClient::open_with_registry(cli.name.clone(), storage, transport, registry)
|
||||
.map_err(|e| anyhow::anyhow!("{e:?}"))
|
||||
.context("failed to open chat client with HTTP registry")?;
|
||||
launch_tui(client, inbound, cli)
|
||||
launch_tui(client, events, cli)
|
||||
}
|
||||
None => {
|
||||
let client = ChatClient::open(cli.name.clone(), storage, transport)
|
||||
let (client, events) = ChatClient::open(cli.name.clone(), storage, transport)
|
||||
.map_err(|e| anyhow::anyhow!("{e:?}"))
|
||||
.context("failed to open chat client")?;
|
||||
launch_tui(client, inbound, cli)
|
||||
launch_tui(client, events, cli)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn launch_tui<D, R>(
|
||||
client: ChatClient<D, R>,
|
||||
inbound: mpsc::Receiver<Vec<u8>>,
|
||||
cli: &Cli,
|
||||
) -> Result<()>
|
||||
fn launch_tui<T, R>(client: ChatClient<T, R>, events: Receiver<Event>, cli: &Cli) -> Result<()>
|
||||
where
|
||||
D: DeliveryService + 'static,
|
||||
R: RegistrationService + 'static,
|
||||
T: DeliveryService + Send + 'static,
|
||||
R: RegistrationService + Send + 'static,
|
||||
{
|
||||
let mut app = ChatApp::new(client, inbound, &cli.name, &cli.data)?;
|
||||
let mut app = ChatApp::new(client, events, &cli.name, &cli.data)?;
|
||||
|
||||
if cli.smoketest {
|
||||
return Ok(());
|
||||
@@ -168,8 +161,7 @@ fn run_logos_delivery(cli: Cli) -> Result<()> {
|
||||
tcp_port: cli.port,
|
||||
..Default::default()
|
||||
};
|
||||
let (delivery, inbound) =
|
||||
Service::start(logos_cfg).context("failed to start logos-delivery")?;
|
||||
let delivery = Service::start(logos_cfg).context("failed to start logos-delivery")?;
|
||||
|
||||
eprintln!("Node connected. Initializing chat client...");
|
||||
|
||||
@@ -180,7 +172,7 @@ fn run_logos_delivery(cli: Cli) -> Result<()> {
|
||||
.map(|p| p.to_path_buf())
|
||||
.unwrap_or_else(|| cli.data.clone());
|
||||
|
||||
let client = match cli.db {
|
||||
let (client, events) = match cli.db {
|
||||
Some(ref path) => {
|
||||
let db_str = path
|
||||
.to_str()
|
||||
@@ -200,7 +192,7 @@ fn run_logos_delivery(cli: Cli) -> Result<()> {
|
||||
None => logos_chat::ChatClient::new(cli.name.clone(), delivery),
|
||||
};
|
||||
|
||||
let mut app = ChatApp::new(client, inbound, &cli.name, &data_dir)?;
|
||||
let mut app = ChatApp::new(client, events, &cli.name, &data_dir)?;
|
||||
|
||||
if cli.smoketest {
|
||||
return Ok(());
|
||||
@@ -219,10 +211,10 @@ fn run_logos_delivery(cli: Cli) -> Result<()> {
|
||||
)
|
||||
}
|
||||
|
||||
fn run_app<D, R>(terminal: &mut ui::Tui, app: &mut ChatApp<D, R>) -> Result<()>
|
||||
fn run_app<T, R>(terminal: &mut ui::Tui, app: &mut ChatApp<T, R>) -> Result<()>
|
||||
where
|
||||
D: DeliveryService + 'static,
|
||||
R: RegistrationService + 'static,
|
||||
T: DeliveryService + Send + 'static,
|
||||
R: RegistrationService + Send + 'static,
|
||||
{
|
||||
loop {
|
||||
app.process_incoming()?;
|
||||
|
||||
@@ -2,11 +2,11 @@ use std::collections::BTreeMap;
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{self, BufReader, Read, Seek, SeekFrom, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use logos_chat::{AddressedEnvelope, DeliveryService};
|
||||
use crossbeam_channel::{Receiver, Sender, bounded};
|
||||
use logos_chat::{AddressedEnvelope, DeliveryService, Transport};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FileTransportError {
|
||||
@@ -17,31 +17,31 @@ pub enum FileTransportError {
|
||||
#[derive(Debug)]
|
||||
pub struct FileTransport {
|
||||
transport_dir: PathBuf,
|
||||
inbound_rx: Option<Receiver<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl FileTransport {
|
||||
/// All instances pointing at the same `transport_dir` share one broadcast bus.
|
||||
///
|
||||
/// Messages are written to `{transport_dir}/{delivery_address}/{hours_since_epoch}.bin`
|
||||
/// as length-prefixed frames (`[u32 BE length][payload bytes]`). The background
|
||||
/// thread reads all files under `transport_dir` and forwards every frame to
|
||||
/// the returned channel; `client.receive()` discards frames it cannot decrypt.
|
||||
pub fn new(transport_dir: &Path) -> io::Result<(Self, mpsc::Receiver<Vec<u8>>)> {
|
||||
/// as length-prefixed frames (`[u32 BE length][payload bytes]`). A background
|
||||
/// thread reads all files under `transport_dir` and forwards every frame to the
|
||||
/// inbound stream the client drains via [`Transport::inbound`] (discarding frames
|
||||
/// it cannot decrypt).
|
||||
pub fn new(transport_dir: &Path) -> io::Result<Self> {
|
||||
fs::create_dir_all(transport_dir)?;
|
||||
|
||||
let (tx, rx) = mpsc::sync_channel(1024);
|
||||
let (tx, rx) = bounded(1024);
|
||||
let dir = transport_dir.to_path_buf();
|
||||
|
||||
thread::Builder::new()
|
||||
.name("file-transport".into())
|
||||
.spawn(move || poll_reader(dir, tx))?;
|
||||
|
||||
Ok((
|
||||
Self {
|
||||
transport_dir: transport_dir.to_path_buf(),
|
||||
},
|
||||
rx,
|
||||
))
|
||||
Ok(Self {
|
||||
transport_dir: transport_dir.to_path_buf(),
|
||||
inbound_rx: Some(rx),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,14 @@ impl DeliveryService for FileTransport {
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for FileTransport {
|
||||
fn inbound(&mut self) -> Receiver<Vec<u8>> {
|
||||
self.inbound_rx
|
||||
.take()
|
||||
.expect("FileTransport::inbound called more than once")
|
||||
}
|
||||
}
|
||||
|
||||
/// Hours since Unix epoch — used as the rolling filename.
|
||||
fn current_hour() -> u64 {
|
||||
SystemTime::now()
|
||||
@@ -77,7 +85,7 @@ fn current_hour() -> u64 {
|
||||
/ 3600
|
||||
}
|
||||
|
||||
fn poll_reader(transport_dir: PathBuf, tx: mpsc::SyncSender<Vec<u8>>) {
|
||||
fn poll_reader(transport_dir: PathBuf, tx: Sender<Vec<u8>>) {
|
||||
// Maps absolute file path → number of bytes already consumed.
|
||||
let mut offsets: BTreeMap<PathBuf, u64> = BTreeMap::new();
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ use std::time::Duration;
|
||||
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||
use logos_chat::{AddressedEnvelope, DeliveryService};
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use logos_chat::{AddressedEnvelope, DeliveryService, Transport};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use wrapper::LogosNodeCtx;
|
||||
@@ -46,7 +47,7 @@ struct OutboundCmd {
|
||||
reply: mpsc::SyncSender<Result<(), DeliveryError>>,
|
||||
}
|
||||
|
||||
type SubscriberList = Arc<Mutex<Vec<mpsc::SyncSender<Vec<u8>>>>>;
|
||||
type SubscriberList = Arc<Mutex<Vec<Sender<Vec<u8>>>>>;
|
||||
|
||||
// ── Config ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -123,18 +124,19 @@ pub struct Service {
|
||||
outbound: mpsc::SyncSender<OutboundCmd>,
|
||||
#[allow(dead_code)]
|
||||
subscribers: SubscriberList,
|
||||
inbound_rx: Option<Receiver<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
/// Start the embedded logos-delivery node. Returns the service and a
|
||||
/// receiver for inbound raw payloads.
|
||||
pub fn start(cfg: Config) -> Result<(Self, mpsc::Receiver<Vec<u8>>), DeliveryError> {
|
||||
/// Start the embedded logos-delivery node. The client drains inbound
|
||||
/// payloads via [`Transport::inbound`].
|
||||
pub fn start(cfg: Config) -> Result<Self, DeliveryError> {
|
||||
let (out_tx, out_rx) = mpsc::sync_channel::<OutboundCmd>(256);
|
||||
let subscribers: SubscriberList = Arc::new(Mutex::new(Vec::new()));
|
||||
let (ready_tx, ready_rx) = mpsc::channel::<Result<(), DeliveryError>>();
|
||||
// Create the inbound channel before spawning so the receiver is
|
||||
// registered inside the thread, before any event callback fires.
|
||||
let (inbound_tx, inbound_rx) = mpsc::sync_channel::<Vec<u8>>(1024);
|
||||
let (inbound_tx, inbound_rx) = crossbeam_channel::bounded::<Vec<u8>>(1024);
|
||||
|
||||
let subs_for_thread = subscribers.clone();
|
||||
|
||||
@@ -167,20 +169,18 @@ impl Service {
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
Ok((
|
||||
Self {
|
||||
outbound: out_tx,
|
||||
subscribers,
|
||||
},
|
||||
inbound_rx,
|
||||
))
|
||||
Ok(Self {
|
||||
outbound: out_tx,
|
||||
subscribers,
|
||||
inbound_rx: Some(inbound_rx),
|
||||
})
|
||||
}
|
||||
|
||||
fn node_thread(
|
||||
cfg: Config,
|
||||
out_rx: mpsc::Receiver<OutboundCmd>,
|
||||
subscribers: SubscriberList,
|
||||
inbound_tx: mpsc::SyncSender<Vec<u8>>,
|
||||
inbound_tx: Sender<Vec<u8>>,
|
||||
ready_tx: mpsc::Sender<Result<(), DeliveryError>>,
|
||||
) {
|
||||
// discv5UdpPort defaults to 9000 in libwaku, so a second instance with
|
||||
@@ -220,8 +220,8 @@ impl Service {
|
||||
};
|
||||
guard.retain(|tx| match tx.try_send(payload.clone()) {
|
||||
Ok(()) => true,
|
||||
Err(mpsc::TrySendError::Full(_)) => true,
|
||||
Err(mpsc::TrySendError::Disconnected(_)) => false,
|
||||
Err(crossbeam_channel::TrySendError::Full(_)) => true,
|
||||
Err(crossbeam_channel::TrySendError::Disconnected(_)) => false,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -306,3 +306,11 @@ impl DeliveryService for Service {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for Service {
|
||||
fn inbound(&mut self) -> Receiver<Vec<u8>> {
|
||||
self.inbound_rx
|
||||
.take()
|
||||
.expect("Service::inbound called more than once")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn restore() -> io::Result<()> {
|
||||
}
|
||||
|
||||
/// Draw the UI.
|
||||
pub fn draw<D: DeliveryService + 'static, R: RegistrationService + 'static>(
|
||||
pub fn draw<D: DeliveryService + Send + 'static, R: RegistrationService + Send + 'static>(
|
||||
frame: &mut Frame,
|
||||
app: &ChatApp<D, R>,
|
||||
) {
|
||||
@@ -58,7 +58,7 @@ pub fn draw<D: DeliveryService + 'static, R: RegistrationService + 'static>(
|
||||
draw_status(frame, app, chunks[3]);
|
||||
}
|
||||
|
||||
fn draw_header<D: DeliveryService + 'static, R: RegistrationService + 'static>(
|
||||
fn draw_header<D: DeliveryService + Send + 'static, R: RegistrationService + Send + 'static>(
|
||||
frame: &mut Frame,
|
||||
app: &ChatApp<D, R>,
|
||||
area: Rect,
|
||||
@@ -85,7 +85,7 @@ fn draw_header<D: DeliveryService + 'static, R: RegistrationService + 'static>(
|
||||
frame.render_widget(header, area);
|
||||
}
|
||||
|
||||
fn draw_messages<D: DeliveryService + 'static, R: RegistrationService + 'static>(
|
||||
fn draw_messages<D: DeliveryService + Send + 'static, R: RegistrationService + Send + 'static>(
|
||||
frame: &mut Frame,
|
||||
app: &ChatApp<D, R>,
|
||||
area: Rect,
|
||||
@@ -175,7 +175,7 @@ fn draw_messages<D: DeliveryService + 'static, R: RegistrationService + 'static>
|
||||
frame.render_stateful_widget(messages_widget, area, &mut list_state);
|
||||
}
|
||||
|
||||
fn draw_input<D: DeliveryService + 'static, R: RegistrationService + 'static>(
|
||||
fn draw_input<D: DeliveryService + Send + 'static, R: RegistrationService + Send + 'static>(
|
||||
frame: &mut Frame,
|
||||
app: &ChatApp<D, R>,
|
||||
area: Rect,
|
||||
@@ -206,7 +206,7 @@ fn draw_input<D: DeliveryService + 'static, R: RegistrationService + 'static>(
|
||||
frame.set_cursor_position((cursor_x, area.y + 1));
|
||||
}
|
||||
|
||||
fn draw_status<D: DeliveryService + 'static, R: RegistrationService + 'static>(
|
||||
fn draw_status<D: DeliveryService + Send + 'static, R: RegistrationService + Send + 'static>(
|
||||
frame: &mut Frame,
|
||||
app: &ChatApp<D, R>,
|
||||
area: Rect,
|
||||
@@ -220,7 +220,10 @@ fn draw_status<D: DeliveryService + 'static, R: RegistrationService + 'static>(
|
||||
}
|
||||
|
||||
/// Handle keyboard events.
|
||||
pub fn handle_events<D: DeliveryService + 'static, R: RegistrationService + 'static>(
|
||||
pub fn handle_events<
|
||||
D: DeliveryService + Send + 'static,
|
||||
R: RegistrationService + Send + 'static,
|
||||
>(
|
||||
app: &mut ChatApp<D, R>,
|
||||
) -> io::Result<bool> {
|
||||
// Poll for events with a short timeout to allow checking incoming messages
|
||||
|
||||
Reference in New Issue
Block a user