1#![cfg(not(target_arch = "wasm32"))]
18
19use std::net::SocketAddr;
20use std::path::PathBuf;
21use std::sync::mpsc::{channel, Receiver, RecvTimeoutError, Sender, TryRecvError};
22use std::thread::JoinHandle;
23use std::time::{Duration, Instant};
24
25use qualia_core_db::p2p::mesh_datagram::{decode_datagram, encode_datagram, ports};
26use qualia_core_db::p2p::mesh_service::{MeshControl, MeshService};
27
28use crate::chat_mesh::ChatMeshBridge;
29use crate::chat_relay::RelayEnvelope;
30
31const LOOP_SLEEP: Duration = Duration::from_millis(10);
33
34pub type IncomingChat = (String, RelayEnvelope);
36
37enum Sink {
39 Channel(Sender<IncomingChat>),
41 Apply(PathBuf),
43}
44
45enum Cmd {
46 Publish {
47 peers: Vec<String>,
48 env: Box<RelayEnvelope>,
49 },
50 Shutdown,
51}
52
53pub struct ChatMeshService {
55 control: MeshControl,
56 cmd_tx: Sender<Cmd>,
57 inbound_rx: Option<Receiver<IncomingChat>>,
59 handle: Option<JoinHandle<()>>,
60}
61
62impl ChatMeshService {
63 pub fn spawn(mesh: MeshService) -> ChatMeshService {
69 let (inbound_tx, inbound_rx) = channel::<IncomingChat>();
70 Self::spawn_with_sink(mesh, Sink::Channel(inbound_tx), Some(inbound_rx))
71 }
72
73 pub fn spawn_applying(mesh: MeshService, storage_root: PathBuf) -> ChatMeshService {
80 Self::spawn_with_sink(mesh, Sink::Apply(storage_root), None)
81 }
82
83 fn spawn_with_sink(
84 mesh: MeshService,
85 sink: Sink,
86 inbound_rx: Option<Receiver<IncomingChat>>,
87 ) -> ChatMeshService {
88 let control = mesh.control();
89 let (cmd_tx, cmd_rx) = channel::<Cmd>();
90 let handle = std::thread::Builder::new()
91 .name("chat-mesh".into())
92 .spawn(move || run(mesh, &cmd_rx, sink))
93 .expect("spawn chat-mesh thread");
94 ChatMeshService {
95 control,
96 cmd_tx,
97 inbound_rx,
98 handle: Some(handle),
99 }
100 }
101
102 pub fn add_peer(
106 &self,
107 peer_id: &str,
108 peer_pubkey_hex: &str,
109 endpoint: Option<SocketAddr>,
110 ) -> Result<SocketAddr, String> {
111 self.control.add_peer(peer_id, peer_pubkey_hex, endpoint)
112 }
113
114 pub fn set_peer_endpoint(&self, peer_id: &str, addr: SocketAddr) -> Result<(), String> {
116 self.control.set_peer_endpoint(peer_id, addr)
117 }
118
119 pub fn initiate_handshake(&self, peer_id: &str) -> Result<(), String> {
121 self.control.initiate_handshake(peer_id)
122 }
123
124 pub fn peers(&self) -> Result<Vec<String>, String> {
126 self.control.peers()
127 }
128
129 pub fn has_session(&self, peer_id: &str) -> Result<bool, String> {
131 self.control.has_session(peer_id)
132 }
133
134 pub fn wait_for_session(&self, peer_id: &str, timeout: Duration) -> bool {
136 self.control.wait_for_session(peer_id, timeout)
137 }
138
139 pub fn publish(&self, peers: Vec<String>, env: RelayEnvelope) -> Result<(), String> {
143 self.cmd_tx
144 .send(Cmd::Publish {
145 peers,
146 env: Box::new(env),
147 })
148 .map_err(|_| "chat-mesh thread is gone".to_string())
149 }
150
151 pub fn try_recv(&self) -> Option<IncomingChat> {
153 self.inbound_rx.as_ref().and_then(|rx| rx.try_recv().ok())
154 }
155
156 pub fn recv_timeout(&self, timeout: Duration) -> Option<IncomingChat> {
158 let rx = self.inbound_rx.as_ref()?;
159 match rx.recv_timeout(timeout) {
160 Ok(v) => Some(v),
161 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => None,
162 }
163 }
164
165 pub fn shutdown(&mut self) {
167 let _ = self.cmd_tx.send(Cmd::Shutdown);
168 if let Some(h) = self.handle.take() {
169 let _ = h.join();
170 }
171 }
172}
173
174impl Drop for ChatMeshService {
175 fn drop(&mut self) {
176 self.shutdown();
177 }
178}
179
180fn send_chat_frame(mesh: &MeshService, peer: &str, frame: &[u8]) {
182 let dgram = encode_datagram(ports::CHAT, ports::CHAT, frame);
183 let _ = mesh.send(peer, dgram);
184}
185
186fn deliver(sink: &Sink, peer_id: &str, env: RelayEnvelope) -> bool {
187 match sink {
188 Sink::Channel(tx) => tx.send((peer_id.to_string(), env)).is_ok(),
189 Sink::Apply(root) => {
190 let _ = crate::chat_relay::apply_incoming_envelope(root, &env.session_id, &env);
192 true
193 }
194 }
195}
196
197fn run(mesh: MeshService, cmd_rx: &Receiver<Cmd>, sink: Sink) {
198 let mut bridge = ChatMeshBridge::default();
199 let start = Instant::now();
200
201 loop {
202 let now = start.elapsed().as_millis() as u64;
203
204 loop {
206 match cmd_rx.try_recv() {
207 Ok(Cmd::Publish { peers, env }) => {
208 for out in bridge.broadcast(&peers, &env, now) {
209 send_chat_frame(&mesh, &out.peer_did, &out.frame);
210 }
211 }
212 Ok(Cmd::Shutdown) | Err(TryRecvError::Disconnected) => return,
213 Err(TryRecvError::Empty) => break,
214 }
215 }
216
217 while let Some(pkt) = mesh.try_recv() {
219 let Some(d) = decode_datagram(&pkt.inner) else {
220 continue;
221 };
222 if d.dst_port != ports::CHAT {
223 continue; }
225 let inb = bridge.on_inbound(&pkt.peer_id, &d.payload, now);
226 for ack in inb.acks {
227 send_chat_frame(&mesh, &ack.peer_did, &ack.frame);
228 }
229 if let Some(env) = inb.delivered {
230 if !deliver(&sink, &pkt.peer_id, env) {
231 return; }
233 }
234 }
235
236 for out in bridge.on_tick(now) {
238 send_chat_frame(&mesh, &out.peer_did, &out.frame);
239 }
240
241 std::thread::sleep(LOOP_SLEEP);
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use qualia_core_db::p2p::social_webnet::SocialWebNet;
249 use qualia_core_db::p2p::wireguard_userspace::generate_keypair;
250
251 fn envelope(content: &str) -> RelayEnvelope {
252 RelayEnvelope {
253 session_id: "room-1".into(),
254 lamport: 5,
255 role: "user".into(),
256 content: content.into(),
257 author_did: "did:wf:alice".into(),
258 author_name: Some("Alice".into()),
259 reply_to_fragment: None,
260 timestamp: 1_700_000_000,
261 signature_hex: "sig".into(),
262 sub_agent_of: None,
263 agent_did: None,
264 model_id: None,
265 agent_backend: None,
266 outcome_sharing: None,
267 }
268 }
269
270 #[test]
274 fn chat_envelope_flows_node_to_node_over_the_mesh() {
275 let a_keys = generate_keypair();
276 let b_keys = generate_keypair();
277 let (a_pub, b_pub) = (a_keys.public_hex(), b_keys.public_hex());
278
279 let to = Some(Duration::from_millis(50));
280 let ip = "127.0.0.1".parse().unwrap();
281 let a_mesh = MeshService::spawn(SocialWebNet::new(a_keys, ip, to));
282 let b_mesh = MeshService::spawn(SocialWebNet::new(b_keys, ip, to));
283
284 let a_local = a_mesh.add_peer("did:wf:bob", &b_pub, None).unwrap();
285 let b_local = b_mesh.add_peer("did:wf:alice", &a_pub, None).unwrap();
286 a_mesh.set_peer_endpoint("did:wf:bob", b_local).unwrap();
287 b_mesh.set_peer_endpoint("did:wf:alice", a_local).unwrap();
288 a_mesh.initiate_handshake("did:wf:bob").unwrap();
289 assert!(a_mesh.wait_for_session("did:wf:bob", Duration::from_secs(3)));
290 assert!(b_mesh.wait_for_session("did:wf:alice", Duration::from_secs(3)));
291
292 let alice = ChatMeshService::spawn(a_mesh);
293 let bob = ChatMeshService::spawn(b_mesh);
294
295 let env = envelope("hello Bob, over the SocialWebNet");
296 alice
297 .publish(vec!["did:wf:bob".to_string()], env.clone())
298 .unwrap();
299
300 let (from, got) = bob
301 .recv_timeout(Duration::from_secs(3))
302 .expect("Bob received a chat envelope");
303 assert_eq!(from, "did:wf:alice");
304 assert_eq!(got.content, "hello Bob, over the SocialWebNet");
305 assert_eq!(got.session_id, "room-1");
306 }
307}