use std::collections::{BTreeMap, BTreeSet};
use std::sync::{Arc, Mutex, MutexGuard};
use futures::channel::mpsc::UnboundedSender;
use serde::{Deserialize, Serialize};
use vertep_core::datamodel::{Event, EventType};
use crate::address::Address;
use crate::json::JsonValue;
use crate::mailbox::{Envelope, Mailbox, MailboxError};
use crate::node::NodeHandle;
use crate::transport::{Resolver, Transport, TransportError};
#[cfg(feature = "websocket")]
pub mod server;
pub const WEBSOCKET_PROCESSOR: &str = "https://w3id.org/vertep/vt#WebSocketEventProcessor";
pub const REFUSED_REASON: &str = "transport-refused";
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Message {
pub id: String,
pub target: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub origin: Option<String>,
pub event: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<JsonValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sendid: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub correlation: Option<String>,
}
impl Message {
pub fn from_envelope(env: &Envelope) -> Self {
Message {
id: env.message_id.clone(),
target: env.to.clone(),
origin: env.event.origin.clone(),
event: env.event.name.clone(),
data: Some(JsonValue(env.event.data.clone())),
sendid: env.event.sendid.clone(),
correlation: env.correlation.clone(),
}
}
pub fn into_envelope(self) -> Envelope {
let mut event = Event::named(self.event, EventType::External);
event.origin = self.origin;
event.origintype = Some(WEBSOCKET_PROCESSOR.to_owned());
event.sendid = self.sendid;
if let Some(d) = self.data {
event.data = d.0;
}
Envelope {
message_id: self.id,
to: self.target,
event,
correlation: self.correlation,
timer: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Frame {
Hello {
node: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
credential: Option<String>,
#[serde(default)]
hosts: Vec<String>,
},
Welcome {
node: String,
#[serde(default)]
hosts: Vec<String>,
},
Refused {
reason: String,
},
Message(Message),
Ack {
id: String,
},
}
impl Frame {
pub fn to_text(&self) -> String {
serde_json::to_string(self).unwrap_or_default()
}
pub fn parse(text: &str) -> Result<Frame, String> {
serde_json::from_str(text).map_err(|e| format!("not a Vertep WebSocket frame: {e}"))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Offer {
Delivered,
Pending,
Refused(String),
}
#[derive(Clone, Debug, PartialEq)]
pub enum Inbound {
Deliver(Envelope),
Acked {
id: String,
watched: bool,
},
Handshake(Frame),
Refused {
reason: String,
failed: Vec<String>,
},
}
#[derive(Debug)]
struct Pending {
env: Envelope,
sent_on: Option<u64>,
order: u64,
}
#[derive(Default)]
struct State {
connection: Option<(u64, UnboundedSender<String>)>,
pending: BTreeMap<String, Pending>,
acked: BTreeSet<String>,
watched: BTreeSet<String>,
settled: Vec<(String, Result<(), String>)>,
refused: Option<String>,
next_order: u64,
routes: BTreeSet<String>,
}
#[derive(Default)]
pub struct Link {
state: Mutex<State>,
}
impl std::fmt::Debug for Link {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = self.lock();
f.debug_struct("Link")
.field("connected", &s.connection.as_ref().map(|c| c.0))
.field("pending", &s.pending.len())
.field("refused", &s.refused)
.finish()
}
}
impl Link {
pub fn new() -> Self {
Link::default()
}
fn lock(&self) -> MutexGuard<'_, State> {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn send_pending(s: &mut State, id: &str) {
let Some((generation, tx)) = s.connection.clone() else {
return;
};
if let Some(p) = s.pending.get_mut(id)
&& p.sent_on != Some(generation)
{
p.sent_on = Some(generation);
let _ = tx.unbounded_send(Frame::Message(Message::from_envelope(&p.env)).to_text());
}
}
pub fn offer(&self, env: Envelope) -> Offer {
let mut s = self.lock();
if s.acked.remove(&env.message_id) {
return Offer::Delivered;
}
if let Some(r) = &s.refused {
return Offer::Refused(r.clone());
}
let id = env.message_id.clone();
if !s.pending.contains_key(&id) {
let order = s.next_order;
s.next_order += 1;
s.pending.insert(
id.clone(),
Pending {
env,
sent_on: None,
order,
},
);
}
Link::send_pending(&mut s, &id);
Offer::Pending
}
pub fn watch(&self, id: &str) {
let mut s = self.lock();
if s.acked.remove(id) {
s.settled.push((id.to_owned(), Ok(())));
} else {
s.watched.insert(id.to_owned());
}
}
pub fn take_settled(&self) -> Vec<(String, Result<(), String>)> {
std::mem::take(&mut self.lock().settled)
}
pub fn is_pending(&self, id: &str) -> bool {
self.lock().pending.contains_key(id)
}
pub fn pending_len(&self) -> usize {
self.lock().pending.len()
}
pub fn connected(&self, generation: u64, tx: UnboundedSender<String>) {
let mut s = self.lock();
s.connection = Some((generation, tx));
s.refused = None;
let mut ids: Vec<(u64, String)> = s
.pending
.iter()
.map(|(k, p)| (p.order, k.clone()))
.collect();
ids.sort();
for (_, id) in ids {
Link::send_pending(&mut s, &id);
}
}
pub fn disconnected(&self, generation: u64) {
let mut s = self.lock();
if s.connection.as_ref().is_some_and(|c| c.0 == generation) {
s.connection = None;
}
}
pub fn is_connected(&self) -> bool {
self.lock().connection.is_some()
}
pub fn ack(&self, id: &str) {
let s = self.lock();
if let Some((_, tx)) = &s.connection {
let _ = tx.unbounded_send(Frame::Ack { id: id.to_owned() }.to_text());
}
}
pub fn send_frame(&self, f: &Frame) -> bool {
let s = self.lock();
match &s.connection {
Some((_, tx)) => tx.unbounded_send(f.to_text()).is_ok(),
None => false,
}
}
pub fn refused(&self, reason: &str) -> Vec<String> {
let mut s = self.lock();
s.refused = Some(reason.to_owned());
let ids: Vec<String> = s.pending.keys().cloned().collect();
s.pending.clear();
let mut failed = Vec::new();
for id in ids {
if s.watched.remove(&id) {
s.settled.push((id.clone(), Err(reason.to_owned())));
failed.push(id);
}
}
failed
}
pub fn refusal(&self) -> Option<String> {
self.lock().refused.clone()
}
pub fn learn_route(&self, iri: &str) {
self.lock().routes.insert(iri.to_owned());
}
pub fn routes_to(&self, iri: &str) -> bool {
self.lock().routes.contains(iri)
}
pub fn receive(&self, text: &str) -> Result<Inbound, String> {
Ok(match Frame::parse(text)? {
Frame::Message(m) => {
if let Some(o) = &m.origin {
self.learn_route(o);
}
Inbound::Deliver(m.into_envelope())
}
Frame::Ack { id } => {
let mut s = self.lock();
let known = s.pending.remove(&id).is_some();
let watched = s.watched.remove(&id);
if watched {
s.settled.push((id.clone(), Ok(())));
} else if known {
s.acked.insert(id.clone());
}
Inbound::Acked { id, watched }
}
Frame::Refused { reason } => {
let failed = self.refused(&reason);
Inbound::Refused { reason, failed }
}
f @ (Frame::Hello { .. } | Frame::Welcome { .. }) => Inbound::Handshake(f),
})
}
}
pub struct LinkMailbox {
link: Arc<Link>,
handle: Option<NodeHandle>,
}
impl std::fmt::Debug for LinkMailbox {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LinkMailbox")
.field("link", &self.link)
.finish_non_exhaustive()
}
}
impl LinkMailbox {
pub fn new(link: Arc<Link>, handle: Option<NodeHandle>) -> Self {
LinkMailbox { link, handle }
}
}
impl Mailbox for LinkMailbox {
fn enqueue(&self, env: Envelope) -> Result<(), MailboxError> {
let (to, copy) = (env.to.clone(), env.clone());
match self.link.offer(env) {
Offer::Delivered => Ok(()),
Offer::Pending => Err(MailboxError(
"awaiting the WebSocket peer's acknowledgement".into(),
)),
Offer::Refused(_) => {
if let (Some(h), Ok(a)) = (&self.handle, Address::parse(&to)) {
h.record_dead_letter(&a, ©, REFUSED_REASON);
}
Ok(())
}
}
}
fn receive(&self, _deferred: &[String]) -> Option<Envelope> {
None
}
fn ack(&self, _message_id: &str) -> Result<(), MailboxError> {
Ok(())
}
fn release(&self, _message_id: &str) {}
fn release_all(&self) {}
fn pending(&self) -> Vec<Envelope> {
Vec::new()
}
fn pending_deliverable(&self, _deferred: &[String]) -> usize {
0
}
}
pub struct ClientResolver {
link: Arc<Link>,
handle: Option<NodeHandle>,
targets: Vec<String>,
}
impl std::fmt::Debug for ClientResolver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClientResolver")
.field("targets", &self.targets)
.finish_non_exhaustive()
}
}
impl ClientResolver {
pub fn new(link: Arc<Link>, handle: Option<NodeHandle>, targets: Vec<String>) -> Self {
ClientResolver {
link,
handle,
targets,
}
}
pub fn claims(&self, address: &str) -> bool {
self.targets.is_empty() || self.targets.iter().any(|t| address.starts_with(t.as_str()))
}
}
impl Resolver for ClientResolver {
fn resolve(&self, address: &Address) -> Option<Arc<dyn Mailbox>> {
self.claims(address.as_str()).then(|| {
Arc::new(LinkMailbox::new(self.link.clone(), self.handle.clone())) as Arc<dyn Mailbox>
})
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct WebSocketTransport;
impl Transport for WebSocketTransport {
fn type_iri(&self) -> &str {
WEBSOCKET_PROCESSOR
}
fn aliases(&self) -> Vec<String> {
vec!["websocket".to_owned()]
}
fn deliver(&self, mailbox: &dyn Mailbox, env: Envelope) -> Result<(), TransportError> {
mailbox
.enqueue(env)
.map_err(|e| TransportError(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures::channel::mpsc::unbounded;
use vertep_core::datamodel::Value;
fn env(id: &str) -> Envelope {
let mut e = Event::named("ping", EventType::External);
e.origin = Some("https://client.example/a/1".into());
e.data = Value::Object(vec![("n".into(), Value::Number(1.0))]);
Envelope::new(
id,
&Address::parse("https://server.example/s/1").unwrap(),
e,
)
}
fn drain(rx: &mut futures::channel::mpsc::UnboundedReceiver<String>) -> Vec<Frame> {
let mut out = Vec::new();
while let Ok(t) = rx.try_recv() {
out.push(Frame::parse(&t).unwrap());
}
out
}
#[test]
fn frames_round_trip() {
let m = Message::from_envelope(&env("m1"));
let f = Frame::Message(m.clone());
assert_eq!(Frame::parse(&f.to_text()).unwrap(), f);
let delivered = m.into_envelope();
assert_eq!(
delivered.event.origintype.as_deref(),
Some(WEBSOCKET_PROCESSOR)
);
assert_eq!(
delivered.event.origin.as_deref(),
Some("https://client.example/a/1")
);
assert_eq!(delivered.event.data, env("m1").event.data);
assert!(Frame::parse(r#"{"type":"ack","id":"x"}"#).is_ok());
assert!(Frame::parse("nope").is_err());
}
#[test]
fn pending_until_acked_and_resent_after_reconnect() {
let link = Link::new();
assert_eq!(link.offer(env("m1")), Offer::Pending, "no connection yet");
let (tx, mut rx) = unbounded();
link.connected(1, tx);
assert_eq!(drain(&mut rx).len(), 1, "sent on connect");
assert_eq!(link.offer(env("m1")), Offer::Pending);
assert!(
drain(&mut rx).is_empty(),
"a retry does not resend on the same connection"
);
link.disconnected(1);
let (tx, mut rx) = unbounded();
link.connected(2, tx);
assert_eq!(drain(&mut rx).len(), 1, "resent after reconnect");
link.receive(r#"{"type":"ack","id":"m1"}"#).unwrap();
assert_eq!(link.offer(env("m1")), Offer::Delivered);
assert_eq!(link.pending_len(), 0);
}
#[test]
fn watched_messages_settle_and_fail_on_refusal() {
let link = Link::new();
let (tx, _rx) = unbounded();
link.connected(1, tx);
link.offer(env("a"));
link.watch("a");
link.offer(env("b"));
link.watch("b");
link.receive(r#"{"type":"ack","id":"a"}"#).unwrap();
assert_eq!(link.take_settled(), vec![("a".into(), Ok(()))]);
let r = link
.receive(r#"{"type":"refused","reason":"bad token"}"#)
.unwrap();
assert_eq!(
r,
Inbound::Refused {
reason: "bad token".into(),
failed: vec!["b".into()]
}
);
assert_eq!(
link.take_settled(),
vec![("b".into(), Err("bad token".into()))]
);
assert_eq!(link.offer(env("c")), Offer::Refused("bad token".into()));
let (tx, mut rx) = unbounded();
link.connected(2, tx);
assert_eq!(link.offer(env("c")), Offer::Pending, "accepted again");
assert!(rx.try_recv().is_ok(), "and sent");
}
#[test]
fn received_messages_teach_routes() {
let link = Link::new();
let text = Frame::Message(Message::from_envelope(&env("m"))).to_text();
assert!(matches!(link.receive(&text).unwrap(), Inbound::Deliver(_)));
assert!(link.routes_to("https://client.example/a/1"));
}
}