1use alloc::collections::{BTreeSet, VecDeque};
5use alloc::vec::Vec;
6
7use umsh_core::{ChannelKey, ChannelTag, PayloadType, PublicKey};
8use umsh_mac::{SendOptions, SendReceipt};
9#[cfg(feature = "software-crypto")]
10use umsh_node::{BoundChannel, Transport};
11use umsh_node::{LocalNode, MacBackend, SendProgressTicket};
12
13use crate::chat::{ChannelRegistry, ChatFacade, Outbound, TransmitUpdate};
14use crate::engine::sequence::MessageHandle;
15use crate::engine::{Destination, Transmission};
16use crate::node_adapter::WellKnownChannels;
17use crate::validate::TextProfile;
18
19pub const WELL_KNOWN_CHANNEL_MAX_FLOOD_HOPS: u8 = 10;
21
22pub const TRANSMISSION_STALL_MS: u64 = 60_000;
26
27#[derive(Clone, Copy)]
29pub struct TransmitPolicy {
30 pub default_flood_hops: u8,
33 pub well_known: WellKnownChannels,
34}
35
36pub fn with_flood_reach(options: SendOptions, hops: u8) -> SendOptions {
39 if hops == 0 {
40 options.no_flood()
41 } else {
42 options.with_flood_hops(hops)
43 }
44}
45
46pub fn channel_reach(
50 channels: &ChannelRegistry,
51 well_known: &WellKnownChannels,
52 channel: &ChannelTag,
53 default_hops: u8,
54) -> u8 {
55 let asked = channels.max_flood_hops(channel).unwrap_or(default_hops);
56 match channels.key(channel) {
57 Some(key) if well_known.contains(&key) => asked.min(WELL_KNOWN_CHANNEL_MAX_FLOOD_HOPS),
58 _ => asked,
59 }
60}
61
62#[cfg(feature = "software-crypto")]
67pub fn bound_channel<M: MacBackend>(
68 node: &LocalNode<M>,
69 channels: &ChannelRegistry,
70 channel: &ChannelTag,
71) -> Option<BoundChannel<M>> {
72 let key = channels.key(channel)?;
73 node.bound_channel(&umsh_node::Channel::private(key, ""))
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub struct Stall {
79 pub transmission_id: u32,
80 pub waited_ms: u64,
81 pub occupied: usize,
83 pub window: usize,
84 pub pending: usize,
86}
87
88pub struct TransmitQueue {
95 window: usize,
96 pending: VecDeque<Queued>,
97 in_flight: Vec<InFlight>,
98 pipeline_ready: BTreeSet<[u8; 32]>,
101}
102
103struct Queued {
104 session_id: u64,
105 transmission: Transmission,
106}
107
108struct InFlight {
109 session_id: u64,
110 transmission_id: u32,
111 gate_peer: Option<PublicKey>,
115 ticket: SendProgressTicket,
116 sent_reported: bool,
117 non_ack: bool,
120 queued_at_ms: u64,
125 stall_reported: bool,
126}
127
128impl TransmitQueue {
129 pub fn new(window: usize) -> Self {
132 Self {
133 window: window.max(1),
134 pending: VecDeque::new(),
135 in_flight: Vec::new(),
136 pipeline_ready: BTreeSet::new(),
137 }
138 }
139
140 pub fn enqueue(&mut self, outbound: Outbound) {
141 let session_id = outbound.session_id();
142 self.pending.extend(
143 outbound
144 .into_transmissions()
145 .into_iter()
146 .map(|transmission| Queued {
147 session_id,
148 transmission,
149 }),
150 );
151 }
152
153 pub fn has_pending(&self) -> bool {
154 !self.pending.is_empty()
155 }
156
157 pub fn pending_len(&self) -> usize {
158 self.pending.len()
159 }
160
161 pub fn in_flight_len(&self) -> usize {
162 self.in_flight.len()
163 }
164
165 pub async fn pump<M, P, const SLOTS: usize, const PAGES: usize>(
171 &mut self,
172 node: &LocalNode<M>,
173 chat: &mut ChatFacade<P, SLOTS, PAGES>,
174 policy: &TransmitPolicy,
175 now_ms: u64,
176 ) -> usize
177 where
178 M: MacBackend,
179 P: TextProfile + Clone,
180 {
181 let mut sent_count = 0;
182 while let Some(next) = self.depart(chat.channels(), policy) {
183 let departure = match next {
184 Ok(departure) => departure,
185 Err(undeliverable) => {
186 undeliverable.fail(chat, now_ms);
187 continue;
188 }
189 };
190 let sent = transmit(node, &departure).await;
191 match self.departed(departure, sent, now_ms) {
192 Departed::Sent => sent_count += 1,
193 Departed::Failed(undeliverable) => undeliverable.fail(chat, now_ms),
194 Departed::Busy => break,
195 }
196 }
197 sent_count
198 }
199
200 pub fn depart(
216 &mut self,
217 channels: &ChannelRegistry,
218 policy: &TransmitPolicy,
219 ) -> Option<Result<Departure, Undeliverable>> {
220 if self.in_flight.len() >= self.window {
221 return None;
222 }
223 let queued = self.pending.pop_front()?;
224 let transmission = &queued.transmission;
225 let gate_peer = match transmission.destination {
226 Destination::Peer(peer) => Some(peer),
227 Destination::Channel(_) | Destination::ChannelPeer { .. } => None,
228 };
229 if let Some(peer) = gate_peer
230 && !self.pipeline_ready.contains(&peer.0)
231 && self
232 .in_flight
233 .iter()
234 .any(|entry| entry.gate_peer == Some(peer))
235 {
236 self.pending.push_front(queued);
237 return None;
238 }
239 let route = match transmission.destination {
240 Destination::Peer(peer) => {
241 let options = with_flood_reach(SendOptions::default(), policy.default_flood_hops)
242 .with_ack_requested(true);
243 let options = if self.pipeline_ready.contains(&peer.0) {
244 options
245 } else {
246 options.with_full_source()
247 };
248 Route::Peer { peer, options }
249 }
250 Destination::Channel(channel) => {
251 let Some(key) = channels.key(&channel) else {
252 return Some(Err(Undeliverable::of(&queued)));
253 };
254 let options = channel_options(channels, policy, &channel, &key);
255 Route::Channel { key, options }
256 }
257 Destination::ChannelPeer { channel, peer } => {
261 let Some(key) = channels.key(&channel) else {
262 return Some(Err(Undeliverable::of(&queued)));
263 };
264 let options = channel_options(channels, policy, &channel, &key);
265 Route::ChannelPeer { key, peer, options }
266 }
267 };
268 let mut payload = Vec::with_capacity(transmission.payload.len() + 1);
269 payload.push(PayloadType::TextMessage as u8);
270 payload.extend_from_slice(transmission.payload.as_slice());
271 Some(Ok(Departure {
272 queued,
273 gate_peer,
274 route,
275 payload,
276 forward_secret: None,
277 }))
278 }
279
280 pub fn departed(&mut self, departure: Departure, sent: Sent, now_ms: u64) -> Departed {
285 let ticket = match sent {
286 Sent::Ticket(ticket) => ticket,
287 Sent::Unsendable => return Departed::Failed(Undeliverable::of(&departure.queued)),
288 Sent::Busy => {
289 self.pending.push_front(departure.queued);
290 return Departed::Busy;
291 }
292 };
293 self.in_flight.push(InFlight {
294 session_id: departure.queued.session_id,
295 transmission_id: departure.queued.transmission.transmission_id,
296 gate_peer: departure.gate_peer,
297 ticket,
298 sent_reported: false,
299 non_ack: departure.gate_peer.is_none(),
300 queued_at_ms: now_ms,
301 stall_reported: false,
302 });
303 Departed::Sent
304 }
305
306 pub fn service<P, const SLOTS: usize, const PAGES: usize>(
309 &mut self,
310 chat: &mut ChatFacade<P, SLOTS, PAGES>,
311 now_ms: u64,
312 ) -> Vec<Stall>
313 where
314 P: TextProfile + Clone,
315 {
316 let occupied = self.in_flight.len();
317 let pending = self.pending.len();
318 let mut stalls = Vec::new();
319 let mut index = 0;
320 while index < self.in_flight.len() {
321 let entry = &mut self.in_flight[index];
322 let waited_ms = now_ms.saturating_sub(entry.queued_at_ms);
325 if !entry.stall_reported
326 && !entry.ticket.was_transmitted()
327 && waited_ms >= TRANSMISSION_STALL_MS
328 {
329 entry.stall_reported = true;
330 stalls.push(Stall {
331 transmission_id: entry.transmission_id,
332 waited_ms,
333 occupied,
334 window: self.window,
335 pending,
336 });
337 }
338 if entry.ticket.was_transmitted() && !entry.sent_reported {
339 entry.sent_reported = true;
340 chat.transmit_update(
341 entry.session_id,
342 entry.transmission_id,
343 TransmitUpdate::Sent,
344 now_ms,
345 );
346 }
347 let entry = &self.in_flight[index];
348 if entry.non_ack && entry.sent_reported {
349 self.in_flight.swap_remove(index);
352 } else if entry.ticket.was_acked() {
353 if let Some(peer) = entry.gate_peer {
354 self.pipeline_ready.insert(peer.0);
355 }
356 chat.transmit_update(
357 entry.session_id,
358 entry.transmission_id,
359 TransmitUpdate::Acked {
360 via_flood: entry.ticket.was_acked_via_route_retry(),
361 },
362 now_ms,
363 );
364 self.in_flight.swap_remove(index);
365 } else if entry.ticket.has_failed() {
366 if let Some(peer) = entry.gate_peer {
369 self.pipeline_ready.remove(&peer.0);
370 }
371 chat.transmit_update(
372 entry.session_id,
373 entry.transmission_id,
374 TransmitUpdate::Failed,
375 now_ms,
376 );
377 self.in_flight.swap_remove(index);
378 } else {
379 index += 1;
380 }
381 }
382 stalls
383 }
384
385 pub fn fail_all<P, const SLOTS: usize, const PAGES: usize>(
389 &mut self,
390 chat: &mut ChatFacade<P, SLOTS, PAGES>,
391 now_ms: u64,
392 ) -> Vec<SendReceipt>
393 where
394 P: TextProfile + Clone,
395 {
396 for queued in self.pending.drain(..) {
397 chat.transmit_update(
398 queued.session_id,
399 queued.transmission.transmission_id,
400 TransmitUpdate::Failed,
401 now_ms,
402 );
403 }
404 let mut receipts = Vec::new();
405 for entry in self.in_flight.drain(..) {
406 if let Some(receipt) = entry.ticket.receipt() {
407 receipts.push(receipt);
408 }
409 chat.transmit_update(
410 entry.session_id,
411 entry.transmission_id,
412 TransmitUpdate::Failed,
413 now_ms,
414 );
415 }
416 receipts
417 }
418}
419
420pub struct Departure {
423 queued: Queued,
424 gate_peer: Option<PublicKey>,
425 route: Route,
426 payload: Vec<u8>,
428 forward_secret: Option<bool>,
431}
432
433impl Departure {
434 pub fn payload(&self) -> &[u8] {
436 self.payload.get(1..).unwrap_or_default()
437 }
438
439 pub fn message(&self) -> Option<MessageHandle> {
442 self.queued.transmission.message
443 }
444
445 pub fn require_forward_secrecy(&mut self, forward_secret: bool) {
449 self.forward_secret = Some(forward_secret);
450 }
451
452 pub fn forward_secrecy(&self) -> Option<bool> {
454 self.forward_secret
455 }
456
457 pub fn peer(&self) -> Option<PublicKey> {
459 match self.route {
460 Route::Peer { peer, .. } => Some(peer),
461 Route::Channel { .. } | Route::ChannelPeer { .. } => None,
462 }
463 }
464}
465
466enum Route {
467 Peer {
468 peer: PublicKey,
469 options: SendOptions,
470 },
471 Channel {
472 key: ChannelKey,
473 options: SendOptions,
474 },
475 ChannelPeer {
476 key: ChannelKey,
477 peer: PublicKey,
478 options: SendOptions,
479 },
480}
481
482pub enum Sent {
484 Ticket(SendProgressTicket),
485 Unsendable,
487 Busy,
489}
490
491#[derive(Clone, Copy, Debug, PartialEq, Eq)]
493pub enum Departed {
494 Sent,
496 Failed(Undeliverable),
498 Busy,
501}
502
503#[derive(Clone, Copy, Debug, PartialEq, Eq)]
506pub struct Undeliverable {
507 pub session_id: u64,
508 pub transmission_id: u32,
509}
510
511impl Undeliverable {
512 fn of(queued: &Queued) -> Self {
513 Self {
514 session_id: queued.session_id,
515 transmission_id: queued.transmission.transmission_id,
516 }
517 }
518
519 pub fn fail<P, const SLOTS: usize, const PAGES: usize>(
521 self,
522 chat: &mut ChatFacade<P, SLOTS, PAGES>,
523 now_ms: u64,
524 ) where
525 P: TextProfile + Clone,
526 {
527 chat.transmit_update(
528 self.session_id,
529 self.transmission_id,
530 TransmitUpdate::Failed,
531 now_ms,
532 );
533 }
534}
535
536pub async fn transmit<M: MacBackend>(node: &LocalNode<M>, departure: &Departure) -> Sent {
539 let payload = &departure.payload[..];
540 match &departure.route {
541 Route::Peer { peer, options } => {
542 let Ok(connection) = node.peer(*peer).await else {
543 return Sent::Unsendable;
544 };
545 match departure.forward_secret {
546 None => connection.send(payload, options).await.into(),
547 Some(forward_secret) => {
548 send_with_secrecy(node, peer, payload, options, forward_secret).await
549 }
550 }
551 }
552 Route::Channel { key, options } => send_on_channel(node, key, None, payload, options).await,
553 Route::ChannelPeer { key, peer, options } => {
554 send_on_channel(node, key, Some(peer), payload, options).await
555 }
556 }
557}
558
559#[cfg(feature = "software-crypto")]
563async fn send_with_secrecy<M: MacBackend>(
564 node: &LocalNode<M>,
565 peer: &PublicKey,
566 payload: &[u8],
567 options: &SendOptions,
568 forward_secret: bool,
569) -> Sent {
570 match node
571 .send_with_secrecy(peer, payload, options, forward_secret)
572 .await
573 {
574 Ok(ticket) => Sent::Ticket(ticket),
575 Err(umsh_node::NodeError::PfsSessionMissing) => Sent::Unsendable,
576 Err(_) => Sent::Busy,
577 }
578}
579
580#[cfg(not(feature = "software-crypto"))]
583async fn send_with_secrecy<M: MacBackend>(
584 node: &LocalNode<M>,
585 peer: &PublicKey,
586 payload: &[u8],
587 options: &SendOptions,
588 forward_secret: bool,
589) -> Sent {
590 if forward_secret {
591 return Sent::Unsendable;
592 }
593 umsh_node::Transport::send(node, peer, payload, options)
594 .await
595 .into()
596}
597
598#[cfg(feature = "software-crypto")]
601async fn send_on_channel<M: MacBackend>(
602 node: &LocalNode<M>,
603 key: &ChannelKey,
604 peer: Option<&PublicKey>,
605 payload: &[u8],
606 options: &SendOptions,
607) -> Sent {
608 let Some(bound) = node.bound_channel(&umsh_node::Channel::private(*key, "")) else {
609 return Sent::Unsendable;
610 };
611 let Some(peer) = peer else {
612 return bound.send_all(payload, options).await.into();
613 };
614 if node.peer(*peer).await.is_err() {
619 return Sent::Unsendable;
620 }
621 bound.send(peer, payload, options).await.into()
622}
623
624#[cfg(not(feature = "software-crypto"))]
626async fn send_on_channel<M: MacBackend>(
627 _node: &LocalNode<M>,
628 _key: &ChannelKey,
629 _peer: Option<&PublicKey>,
630 _payload: &[u8],
631 _options: &SendOptions,
632) -> Sent {
633 Sent::Unsendable
634}
635
636impl<E> From<Result<SendProgressTicket, E>> for Sent {
637 fn from(result: Result<SendProgressTicket, E>) -> Self {
638 match result {
639 Ok(ticket) => Sent::Ticket(ticket),
640 Err(_) => Sent::Busy,
641 }
642 }
643}
644
645fn channel_options(
654 channels: &ChannelRegistry,
655 policy: &TransmitPolicy,
656 channel: &ChannelTag,
657 key: &ChannelKey,
658) -> SendOptions {
659 let reach = channel_reach(
660 channels,
661 &policy.well_known,
662 channel,
663 policy.default_flood_hops,
664 );
665 let options = with_flood_reach(SendOptions::default(), reach).with_full_source();
666 if policy.well_known.is_emergency(key) {
667 options.unencrypted()
668 } else {
669 options
670 }
671}