use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use tokio::sync::oneshot;
use crate::runtime::signals::shutdown_requested;
use crate::transports::contract::OutcomeFuture;
use crate::transports::{
DeliveryEnvelope, OutputView, SendOutcome, SingleDeliveryOutcome, StartupContext, Transport,
TransportError, TransportReadiness, TransportStatus,
};
const DROPPED_ON_SHUTDOWN_REASON: &str = "relay shutdown requested before delivery";
const DROPPED_ON_SHUTDOWN_REASON_CODE: &str = "dropped_on_shutdown";
const UI_RAW_WRITE_UNSUPPORTED_CODE: &str = "ui_raw_write_unsupported";
const UI_RECONNECT_POLL_INTERVAL_MS: u64 = 100;
const UI_RECONNECT_TIMEOUT_MS_DEFAULT: u64 = 30_000;
#[derive(Clone, Debug)]
pub enum UiBroadcastStatus {
Delivered,
NoUi,
Failed(String),
}
#[derive(Clone, Debug)]
pub struct UiIncomingMessage {
pub message_id: String,
pub sender_session: String,
pub body: String,
pub cc_sessions: Vec<String>,
pub authenticated_identity: Option<String>,
}
#[derive(Clone, Debug)]
pub struct UiOutcomePhase {
pub message_id: String,
pub phase: &'static str,
pub outcome: Option<&'static str>,
pub reason_code: Option<String>,
pub reason: Option<String>,
}
pub type UiBroadcastFn = Arc<dyn Fn(&UiIncomingMessage) -> UiBroadcastStatus + Send + Sync>;
pub type UiPhaseFn = Arc<dyn Fn(UiOutcomePhase) -> UiBroadcastStatus + Send + Sync>;
#[derive(Clone)]
pub struct UiTransportServices {
pub broadcast_incoming: UiBroadcastFn,
pub emit_phase: UiPhaseFn,
}
impl std::fmt::Debug for UiTransportServices {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UiTransportServices")
.finish_non_exhaustive()
}
}
#[derive(Debug)]
pub struct UiTransport {
services: UiTransportServices,
}
impl UiTransport {
#[must_use]
pub fn new(services: UiTransportServices) -> Self {
Self { services }
}
}
impl Transport for UiTransport {
fn startup(&mut self, _context: StartupContext) -> Result<TransportStatus, TransportError> {
Ok(TransportStatus {
readiness: TransportReadiness::Ready,
})
}
fn mailw(&mut self, envelope: DeliveryEnvelope) -> OutcomeFuture {
let (sender, receiver) = oneshot::channel();
let services = self.services.clone();
let timeout = envelope
.quiescence_timeout
.unwrap_or(Duration::from_millis(UI_RECONNECT_TIMEOUT_MS_DEFAULT));
let message_id = envelope.message_id.clone();
let message = envelope.message;
let incoming = UiIncomingMessage {
message_id: envelope.message_id,
sender_session: message.sender.session,
body: message.body,
cc_sessions: message.cc.into_iter().map(|party| party.session).collect(),
authenticated_identity: message.authenticated_identity,
};
thread::spawn(move || {
let outcome = run_ui_delivery(&services, timeout, message_id, &incoming);
let _ = sender.send(outcome);
});
receiver
}
fn raww(&mut self, content: String, append_enter: bool) -> OutcomeFuture {
let _ = (content, append_enter);
let (sender, receiver) = oneshot::channel();
let _ = sender.send(SingleDeliveryOutcome {
target_session: String::new(),
message_id: String::new(),
outcome: SendOutcome::Failed,
reason_code: Some(UI_RAW_WRITE_UNSUPPORTED_CODE.to_string()),
reason: Some("UI transport does not support raw input".to_string()),
details: None,
});
receiver
}
fn is_ready(&self) -> bool {
true
}
fn shutdown(&mut self) {}
fn give_output(&self) -> Option<Arc<dyn OutputView>> {
None
}
}
fn run_ui_delivery(
services: &UiTransportServices,
timeout: Duration,
message_id: String,
incoming: &UiIncomingMessage,
) -> SingleDeliveryOutcome {
let start = Instant::now();
loop {
if shutdown_requested() {
let _ = (services.emit_phase)(UiOutcomePhase {
message_id: message_id.clone(),
phase: "failed",
outcome: Some("failed"),
reason_code: Some(DROPPED_ON_SHUTDOWN_REASON_CODE.to_string()),
reason: Some(DROPPED_ON_SHUTDOWN_REASON.to_string()),
});
return terminal(
message_id,
SendOutcome::DroppedOnShutdown,
Some(DROPPED_ON_SHUTDOWN_REASON_CODE.to_string()),
Some(DROPPED_ON_SHUTDOWN_REASON.to_string()),
);
}
match (services.emit_phase)(UiOutcomePhase {
message_id: message_id.clone(),
phase: "routed",
outcome: None,
reason_code: None,
reason: None,
}) {
UiBroadcastStatus::Delivered => {}
UiBroadcastStatus::NoUi => {
if let Some(outcome) = timeout_if_elapsed(&message_id, start, timeout) {
return outcome;
}
thread::sleep(Duration::from_millis(UI_RECONNECT_POLL_INTERVAL_MS));
continue;
}
UiBroadcastStatus::Failed(reason) => {
return terminal(message_id, SendOutcome::Failed, None, Some(reason));
}
}
match (services.broadcast_incoming)(incoming) {
UiBroadcastStatus::Delivered => {
let _ = (services.emit_phase)(UiOutcomePhase {
message_id: message_id.clone(),
phase: "delivered",
outcome: Some("success"),
reason_code: None,
reason: None,
});
return terminal(message_id, SendOutcome::Delivered, None, None);
}
UiBroadcastStatus::NoUi => {}
UiBroadcastStatus::Failed(reason) => {
return terminal(message_id, SendOutcome::Failed, None, Some(reason));
}
}
if let Some(outcome) = timeout_if_elapsed(&message_id, start, timeout) {
return outcome;
}
thread::sleep(Duration::from_millis(UI_RECONNECT_POLL_INTERVAL_MS));
}
}
fn timeout_if_elapsed(
message_id: &str,
start: Instant,
timeout: Duration,
) -> Option<SingleDeliveryOutcome> {
if start.elapsed() >= timeout {
return Some(terminal(
message_id.to_string(),
SendOutcome::Timeout,
None,
Some(format!(
"ui relay stream was disconnected for {}ms",
start.elapsed().as_millis()
)),
));
}
None
}
fn terminal(
message_id: String,
outcome: SendOutcome,
reason_code: Option<String>,
reason: Option<String>,
) -> SingleDeliveryOutcome {
SingleDeliveryOutcome {
target_session: String::new(),
message_id,
outcome,
reason_code,
reason,
details: None,
}
}