use anyhow::{Context, Result};
use serde::Serialize;
use serde_json::{Value, json};
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum SyncDelivery {
Delivered {
event_id: String,
relay_url: String,
slot_id: String,
},
Duplicate {
event_id: String,
relay_url: String,
slot_id: String,
},
PeerUnknown { event_id: String },
SlotStale {
event_id: String,
relay_url: String,
slot_id: String,
detail: String,
},
TransportError {
event_id: String,
relay_url: String,
slot_id: String,
detail: String,
},
}
impl SyncDelivery {
pub fn status_str(&self) -> &'static str {
match self {
SyncDelivery::Delivered { .. } => "delivered",
SyncDelivery::Duplicate { .. } => "duplicate",
SyncDelivery::PeerUnknown { .. } => "peer_unknown",
SyncDelivery::SlotStale { .. } => "slot_stale",
SyncDelivery::TransportError { .. } => "transport_error",
}
}
pub fn reached_relay(&self) -> bool {
matches!(
self,
SyncDelivery::Delivered { .. } | SyncDelivery::Duplicate { .. }
)
}
pub fn event_id(&self) -> &str {
match self {
SyncDelivery::Delivered { event_id, .. }
| SyncDelivery::Duplicate { event_id, .. }
| SyncDelivery::PeerUnknown { event_id }
| SyncDelivery::SlotStale { event_id, .. }
| SyncDelivery::TransportError { event_id, .. } => event_id,
}
}
}
pub fn attempt_deliver(peer_handle: &str, signed_event: &Value) -> Result<SyncDelivery> {
let event_id = signed_event
.get("event_id")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let state = crate::config::read_relay_state().context("reading relay state")?;
let endpoints = crate::endpoints::peer_endpoints_in_priority_order(&state, peer_handle);
if endpoints.is_empty() {
return Ok(SyncDelivery::PeerUnknown { event_id });
}
let mut last_failure: Option<SyncDelivery> = None;
for ep in endpoints {
if ep.relay_url.is_empty() || ep.slot_id.is_empty() || ep.slot_token.is_empty() {
continue;
}
let client = crate::relay_client::RelayClient::new(&ep.relay_url);
match client.post_event(&ep.slot_id, &ep.slot_token, signed_event) {
Ok(resp) => {
let now = time::OffsetDateTime::now_utc()
.format(&time::format_description::well_known::Rfc3339)
.unwrap_or_default();
if let Err(e) = crate::config::append_pushed_log(peer_handle, &event_id, &now) {
eprintln!(
"wire send: pushed-log append for {peer_handle}/{event_id} failed (non-fatal): {e:#}"
);
}
return Ok(if resp.status == "duplicate" {
SyncDelivery::Duplicate {
event_id,
relay_url: ep.relay_url,
slot_id: ep.slot_id,
}
} else {
SyncDelivery::Delivered {
event_id,
relay_url: ep.relay_url,
slot_id: ep.slot_id,
}
});
}
Err(e) => {
let detail = crate::relay_client::format_transport_error(&e);
last_failure = Some(if crate::cli::error_smells_like_slot_4xx(&detail) {
SyncDelivery::SlotStale {
event_id: event_id.clone(),
relay_url: ep.relay_url,
slot_id: ep.slot_id,
detail,
}
} else {
SyncDelivery::TransportError {
event_id: event_id.clone(),
relay_url: ep.relay_url,
slot_id: ep.slot_id,
detail,
}
});
}
}
}
Ok(last_failure.unwrap_or(SyncDelivery::PeerUnknown { event_id }))
}
pub fn delivery_json(d: &SyncDelivery, peer: &str) -> Value {
let base = json!({
"status": d.status_str(),
"peer": peer,
"event_id": d.event_id(),
});
let mut obj = base.as_object().cloned().unwrap_or_default();
match d {
SyncDelivery::Delivered {
relay_url, slot_id, ..
}
| SyncDelivery::Duplicate {
relay_url, slot_id, ..
} => {
obj.insert("relay_url".into(), json!(relay_url));
obj.insert("slot_id".into(), json!(slot_id));
}
SyncDelivery::SlotStale {
relay_url,
slot_id,
detail,
..
}
| SyncDelivery::TransportError {
relay_url,
slot_id,
detail,
..
} => {
obj.insert("relay_url".into(), json!(relay_url));
obj.insert("slot_id".into(), json!(slot_id));
obj.insert("reason".into(), json!(detail));
}
SyncDelivery::PeerUnknown { .. } => {
obj.insert(
"reason".into(),
json!(format!(
"peer '{peer}' not pinned — run `wire dial {peer}` to pair, or pass --queue (CLI) / queue:true (MCP) to write to outbox for the daemon to attempt later"
)),
);
}
}
Value::Object(obj)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_str_matches_variant() {
let d = SyncDelivery::Delivered {
event_id: "x".into(),
relay_url: "https://r".into(),
slot_id: "s".into(),
};
assert_eq!(d.status_str(), "delivered");
assert!(d.reached_relay());
let d = SyncDelivery::Duplicate {
event_id: "x".into(),
relay_url: "https://r".into(),
slot_id: "s".into(),
};
assert_eq!(d.status_str(), "duplicate");
assert!(
d.reached_relay(),
"duplicate counts as relay-reached: peer can pull it"
);
let d = SyncDelivery::PeerUnknown {
event_id: "x".into(),
};
assert_eq!(d.status_str(), "peer_unknown");
assert!(!d.reached_relay());
let d = SyncDelivery::SlotStale {
event_id: "x".into(),
relay_url: "https://r".into(),
slot_id: "s".into(),
detail: "410".into(),
};
assert_eq!(d.status_str(), "slot_stale");
assert!(!d.reached_relay());
let d = SyncDelivery::TransportError {
event_id: "x".into(),
relay_url: "https://r".into(),
slot_id: "s".into(),
detail: "tls".into(),
};
assert_eq!(d.status_str(), "transport_error");
assert!(!d.reached_relay());
}
#[test]
fn delivery_json_includes_reason_only_for_failures() {
let ok = SyncDelivery::Delivered {
event_id: "abc".into(),
relay_url: "https://r".into(),
slot_id: "s".into(),
};
let v = delivery_json(&ok, "alice");
assert_eq!(v["status"], "delivered");
assert_eq!(v["event_id"], "abc");
assert_eq!(v["peer"], "alice");
assert_eq!(v["relay_url"], "https://r");
assert!(v.get("reason").is_none(), "happy path has no reason field");
let bad = SyncDelivery::TransportError {
event_id: "abc".into(),
relay_url: "https://r".into(),
slot_id: "s".into(),
detail: "TLS error: UnknownIssuer".into(),
};
let v = delivery_json(&bad, "alice");
assert_eq!(v["status"], "transport_error");
assert_eq!(v["reason"], "TLS error: UnknownIssuer");
let unknown = SyncDelivery::PeerUnknown {
event_id: "abc".into(),
};
let v = delivery_json(&unknown, "alice");
assert_eq!(v["status"], "peer_unknown");
assert!(
v["reason"]
.as_str()
.unwrap_or("")
.contains("wire dial alice")
);
}
}