use std::sync::Arc;
use std::time::Duration;
use affinidi_tdk::messaging::protocols::message_pickup::InboundFrame;
use affinidi_tdk::messaging::{ATM, profiles::ATMProfile};
use serde_json::Value;
use tracing::{debug, warn};
use trust_tasks_rs::TrustTask;
use trust_tasks_tsp::ENVELOPE_TYPE;
use crate::error::TrqlError;
use crate::pending::PendingReplies;
use crate::transport::{TransportKind, TrqlTransport};
const INBOUND_POLL_WAIT: Duration = Duration::from_secs(10);
const INBOUND_ERROR_BACKOFF: Duration = Duration::from_millis(500);
#[derive(Debug, Clone)]
pub struct TspTransportConfig {
pub reply_timeout: Duration,
}
impl Default for TspTransportConfig {
fn default() -> Self {
Self {
reply_timeout: Duration::from_secs(60),
}
}
}
pub struct TspTransport {
atm: ATM,
profile: Arc<ATMProfile>,
pending: PendingReplies,
reply_timeout: Duration,
demux: tokio::task::JoinHandle<()>,
}
impl TspTransport {
pub fn new(
atm: ATM,
profile: Arc<ATMProfile>,
config: TspTransportConfig,
) -> Result<Self, TrqlError> {
if profile.to_tdk_profile().mediator.is_none() {
return Err(TrqlError::Config(
"profile has no mediator configured (required for the TSP binding)".to_string(),
));
}
let pending = PendingReplies::new();
let demux = tokio::spawn(demux_loop(atm.clone(), profile.clone(), pending.clone()));
Ok(Self {
atm,
profile,
pending,
reply_timeout: config.reply_timeout,
demux,
})
}
}
impl Drop for TspTransport {
fn drop(&mut self) {
self.demux.abort();
}
}
#[async_trait::async_trait]
impl TrqlTransport for TspTransport {
fn kind(&self) -> TransportKind {
TransportKind::Tsp
}
async fn exchange(&self, request: TrustTask<Value>) -> Result<TrustTask<Value>, TrqlError> {
let dest = request.recipient.clone().ok_or_else(|| {
TrqlError::Config("request document has no recipient to route to".to_string())
})?;
let request_id = request.id.clone();
let envelope = build_envelope(&request)?;
let receiver = self.pending.register(&request_id);
if let Err(e) = self.atm.tsp().send(&self.profile, &dest, &envelope).await {
self.pending.abandon(&request_id);
return Err(TrqlError::Transport {
kind: TransportKind::Tsp,
detail: format!("TSP send failed: {e}"),
});
}
match tokio::time::timeout(self.reply_timeout, receiver).await {
Ok(Ok(document)) => Ok(document),
Ok(Err(_closed)) => {
self.pending.abandon(&request_id);
Err(TrqlError::Transport {
kind: TransportKind::Tsp,
detail: "reply demux task stopped".to_string(),
})
}
Err(_elapsed) => {
self.pending.abandon(&request_id);
Err(TrqlError::Timeout {
kind: TransportKind::Tsp,
waited_secs: self.reply_timeout.as_secs(),
})
}
}
}
}
fn build_envelope(document: &TrustTask<Value>) -> Result<Vec<u8>, TrqlError> {
let document = serde_json::to_value(document)
.map_err(|e| TrqlError::Contract(format!("request did not serialize: {e}")))?;
let envelope = serde_json::json!({ "type": ENVELOPE_TYPE, "document": document });
serde_json::to_vec(&envelope)
.map_err(|e| TrqlError::Contract(format!("envelope did not serialize: {e}")))
}
fn parse_envelope(payload: &[u8]) -> Result<TrustTask<Value>, String> {
let envelope: Value =
serde_json::from_slice(payload).map_err(|e| format!("invalid TSP envelope JSON: {e}"))?;
match envelope.get("type").and_then(Value::as_str) {
Some(t) if t == ENVELOPE_TYPE => {}
other => return Err(format!("unexpected TSP envelope type: {other:?}")),
}
let document = envelope
.get("document")
.cloned()
.ok_or_else(|| "TSP envelope missing `document`".to_string())?;
serde_json::from_value(document).map_err(|e| format!("invalid Trust Task document: {e}"))
}
async fn demux_loop(atm: ATM, profile: Arc<ATMProfile>, pending: PendingReplies) {
loop {
match atm
.message_pickup()
.live_stream_next_frame(&profile, Some(INBOUND_POLL_WAIT), true)
.await
{
Ok(Some(InboundFrame::Tsp(packed))) => {
let (payload, sender) = match atm.tsp().unpack(&profile, &packed).await {
Ok(unsealed) => unsealed,
Err(e) => {
warn!("TSP unpack failed: {e}");
continue;
}
};
match parse_envelope(&payload) {
Ok(document) => {
if !pending.route(document) {
debug!("unsolicited TSP Trust Task from {sender} dropped");
}
}
Err(e) => warn!("dropping TSP message from {sender}: {e}"),
}
}
Ok(Some(_)) => {}
Ok(None) => {}
Err(e) => {
debug!("inbound stream error (backing off): {e}");
tokio::time::sleep(INBOUND_ERROR_BACKOFF).await;
}
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn envelope_round_trips() {
let doc = TrustTask::new(
"urn:uuid:1".to_string(),
"https://trusttasks.org/spec/registry/authorization/0.1"
.parse()
.unwrap(),
serde_json::json!({"entity_id": "did:example:e"}),
);
let bytes = build_envelope(&doc).unwrap();
let parsed = parse_envelope(&bytes).unwrap();
assert_eq!(parsed.id, "urn:uuid:1");
assert_eq!(parsed.payload["entity_id"], "did:example:e");
}
#[test]
fn wrong_envelope_type_is_rejected() {
let bytes = serde_json::to_vec(
&serde_json::json!({ "type": "https://example.org/other", "document": {} }),
)
.unwrap();
assert!(parse_envelope(&bytes).is_err());
}
}