lxmf-runtime 0.9.5

In-process LXMF SDK backend over the Reticulum transport runtime.
Documentation
use std::sync::Arc;
use std::time::Duration;

use lxmf_core::MessageMethod;
use lxmf_sdk::{MessageId, SdkError};
use rns_transport::delivery::{await_link_activation, send_on_link, LinkSendResult};
use rns_transport::destination::link::Link;
use rns_transport::destination::DestinationDesc;
use rns_transport::resource::ResourceEventKind;
use rns_transport::transport::Transport;
use tokio::sync::Mutex;

use crate::delivery::{
    transport_error, DeliveryMethod, DeliveryOutcome, DeliveryRepresentation, InProcessSendReport,
};

#[expect(
    clippy::too_many_arguments,
    reason = "transport send contract keeps routing and timeout choices explicit"
)]
pub(crate) async fn send_link_payload(
    transport: &Transport,
    destination: DestinationDesc,
    payload: &[u8],
    message_id: MessageId,
    method: DeliveryMethod,
    representation: MessageMethod,
    link_timeout: Duration,
    link_attempts: usize,
    resource_timeout: Duration,
    propagated_recipient: Option<String>,
) -> Result<InProcessSendReport, SdkError> {
    let resolved_destination = destination.address_hash.to_hex_string();
    let link = activate_link(transport, destination, link_timeout, link_attempts).await?;
    let mut resource_events = transport.resource_events();
    let (actual_representation, receipt_hash) = if matches!(representation, MessageMethod::Resource)
    {
        let link_id = *link.lock().await.id();
        let resource_hash = transport
            .send_resource(&link_id, payload.to_vec(), None)
            .await
            .map_err(|err| transport_error(format!("resource send failed: {err:?}")))?;
        await_resource_completion(&mut resource_events, resource_hash, resource_timeout).await?;
        (DeliveryRepresentation::Resource, None)
    } else {
        match send_on_link(transport, &link, payload)
            .await
            .map_err(|err| transport_error(format!("link send failed: {err}")))?
        {
            LinkSendResult::Packet(packet) => {
                (DeliveryRepresentation::Packet, Some(hex::encode(packet.hash().to_bytes())))
            }
            LinkSendResult::Resource(resource_hash) => {
                await_resource_completion(&mut resource_events, resource_hash, resource_timeout)
                    .await?;
                (DeliveryRepresentation::Resource, None)
            }
        }
    };
    Ok(InProcessSendReport {
        message_id,
        resolved_destination: propagated_recipient.unwrap_or(resolved_destination.clone()),
        method,
        representation: actual_representation,
        outcome: DeliveryOutcome::SentDirect,
        relay_destination: matches!(method, DeliveryMethod::Propagated)
            .then_some(resolved_destination),
        receipt_hash,
    })
}

async fn activate_link(
    transport: &Transport,
    destination: DestinationDesc,
    timeout: Duration,
    attempts: usize,
) -> Result<Arc<Mutex<Link>>, SdkError> {
    let mut last_error = None;
    for _ in 0..attempts.max(1) {
        let link = transport.link(destination).await;
        match await_link_activation(transport, &link, timeout).await {
            Ok(()) => return Ok(link),
            Err(error) => {
                last_error = Some(error);
                transport.reset_out_link(&destination.address_hash).await;
            }
        }
    }
    Err(transport_error(format!(
        "link activation failed: {}",
        last_error.map_or_else(|| "no attempts made".to_owned(), |error| error.to_string())
    )))
}

pub(crate) async fn await_resource_completion(
    events: &mut tokio::sync::broadcast::Receiver<rns_transport::resource::ResourceEvent>,
    resource_hash: rns_transport::hash::Hash,
    timeout: Duration,
) -> Result<(), SdkError> {
    let deadline = tokio::time::Instant::now() + timeout;
    loop {
        let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
        if remaining.is_zero() {
            return Err(transport_error("resource transfer timed out"));
        }
        match tokio::time::timeout(remaining, events.recv()).await {
            Ok(Ok(event)) if event.hash != resource_hash => continue,
            Ok(Ok(event)) => match event.kind {
                ResourceEventKind::OutboundComplete => return Ok(()),
                ResourceEventKind::OutboundFailed => {
                    return Err(transport_error("resource transfer failed"))
                }
                ResourceEventKind::OutboundCancelled => {
                    return Err(transport_error("resource transfer cancelled"))
                }
                _ => continue,
            },
            Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => continue,
            Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) => {
                return Err(transport_error("resource event stream closed"));
            }
            Err(_) => return Err(transport_error("resource transfer timed out")),
        }
    }
}