use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use serde::{Deserialize, Serialize};
pub const DELIVERY_PROTOCOL_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ServerMessage {
Welcome {
protocol_version: u32,
max_known_id: i64,
},
Push {
protocol_version: u32,
id: i64,
kind: String,
recipient: String,
tenant_id: Option<String>,
payload_b64: String,
},
}
impl ServerMessage {
pub fn push_from_row(row: &crate::models::delivery_outbox::DeliveryOutbox) -> Self {
ServerMessage::Push {
protocol_version: DELIVERY_PROTOCOL_VERSION,
id: row.id,
kind: row.kind.clone(),
recipient: row.recipient.clone(),
tenant_id: row.tenant_id.clone(),
payload_b64: BASE64.encode(&row.payload),
}
}
pub fn decode_push_payload(&self) -> Result<Vec<u8>, EnvelopeError> {
match self {
ServerMessage::Push { payload_b64, .. } => BASE64
.decode(payload_b64)
.map_err(|e| EnvelopeError::Base64(e.to_string())),
_ => Err(EnvelopeError::WrongVariant("Push")),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ClientMessage {
Hello {
protocol_version: u32,
since_id: Option<i64>,
},
Ack { protocol_version: u32, id: i64 },
}
#[derive(Debug, thiserror::Error)]
pub enum EnvelopeError {
#[error("expected envelope variant {0}")]
WrongVariant(&'static str),
#[error("invalid base64 payload: {0}")]
Base64(String),
#[error("invalid JSON envelope: {0}")]
Json(#[from] serde_json::Error),
#[error("unsupported protocol_version: got {got}, supports {supported}")]
UnsupportedVersion { got: u32, supported: u32 },
}
#[cfg(test)]
mod tests {
use super::*;
use crate::database::universal_types::UniversalTimestamp;
use crate::models::delivery_outbox::DeliveryOutbox;
#[test]
fn welcome_round_trips_as_json() {
let msg = ServerMessage::Welcome {
protocol_version: DELIVERY_PROTOCOL_VERSION,
max_known_id: 42,
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"type\":\"welcome\""));
let back: ServerMessage = serde_json::from_str(&json).unwrap();
assert_eq!(back, msg);
}
#[test]
fn push_round_trips_with_base64_payload() {
let row = DeliveryOutbox {
id: 7,
recipient: "agent:abc".to_string(),
kind: "work".to_string(),
tenant_id: Some("t1".to_string()),
payload: b"\x00\xffhello".to_vec(),
delivery_state: "pending".to_string(),
delivery_attempts: 0,
created_at: UniversalTimestamp::now(),
delivered_at: None,
acked_at: None,
};
let msg = ServerMessage::push_from_row(&row);
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"type\":\"push\""));
let back: ServerMessage = serde_json::from_str(&json).unwrap();
assert_eq!(back.decode_push_payload().unwrap(), row.payload);
}
#[test]
fn ack_and_hello_round_trip() {
let ack = ClientMessage::Ack {
protocol_version: DELIVERY_PROTOCOL_VERSION,
id: 99,
};
let json = serde_json::to_string(&ack).unwrap();
assert!(json.contains("\"type\":\"ack\""));
assert_eq!(serde_json::from_str::<ClientMessage>(&json).unwrap(), ack);
let hello = ClientMessage::Hello {
protocol_version: DELIVERY_PROTOCOL_VERSION,
since_id: Some(10),
};
let json = serde_json::to_string(&hello).unwrap();
assert!(json.contains("\"type\":\"hello\""));
assert_eq!(serde_json::from_str::<ClientMessage>(&json).unwrap(), hello);
let hello_none = ClientMessage::Hello {
protocol_version: DELIVERY_PROTOCOL_VERSION,
since_id: None,
};
let json = serde_json::to_string(&hello_none).unwrap();
assert_eq!(
serde_json::from_str::<ClientMessage>(&json).unwrap(),
hello_none
);
}
#[test]
fn decode_push_payload_rejects_wrong_variant() {
let msg = ServerMessage::Welcome {
protocol_version: DELIVERY_PROTOCOL_VERSION,
max_known_id: 0,
};
assert!(matches!(
msg.decode_push_payload(),
Err(EnvelopeError::WrongVariant("Push"))
));
}
}