use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct VectorClock(pub BTreeMap<String, u64>);
impl VectorClock {
pub fn new() -> Self {
Self(BTreeMap::new())
}
pub fn increment(&mut self, peer_id: &str) {
let counter = self.0.entry(peer_id.to_string()).or_insert(0);
*counter += 1;
}
pub fn merge(&mut self, other: &VectorClock) {
for (peer, timestamp) in &other.0 {
let entry = self.0.entry(peer.clone()).or_insert(0);
*entry = (*entry).max(*timestamp);
}
}
pub fn compare(&self, other: &VectorClock) -> ClockOrdering {
let mut self_less = false;
let mut other_less = false;
let mut all_peers: std::collections::HashSet<&String> = self.0.keys().collect();
all_peers.extend(other.0.keys());
for peer in all_peers {
let self_val = self.0.get(peer).copied().unwrap_or(0);
let other_val = other.0.get(peer).copied().unwrap_or(0);
match self_val.cmp(&other_val) {
Ordering::Less => other_less = true,
Ordering::Greater => self_less = true,
Ordering::Equal => {}
}
}
match (self_less, other_less) {
(true, true) => ClockOrdering::Concurrent, (true, false) => ClockOrdering::After, (false, true) => ClockOrdering::Before, (false, false) => ClockOrdering::Equal, }
}
pub fn has_dependencies(&self, message_clock: &VectorClock) -> bool {
for (peer, timestamp) in &message_clock.0 {
let our_timestamp = self.0.get(peer).copied().unwrap_or(0);
if our_timestamp < timestamp.saturating_sub(1) {
return false; }
}
true
}
pub fn get_missing_ranges(&self, remote: &VectorClock) -> Vec<MissingRange> {
let mut missing = Vec::new();
for (peer_id, remote_ts) in &remote.0 {
let local_ts = self.0.get(peer_id).copied().unwrap_or(0);
if *remote_ts > local_ts {
missing.push(MissingRange {
peer_id: peer_id.clone(),
from_timestamp: local_ts + 1,
to_timestamp: *remote_ts,
});
}
}
missing
}
}
impl Default for VectorClock {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClockOrdering {
Before, After, Concurrent, Equal, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MissingRange {
pub peer_id: String,
pub from_timestamp: u64,
pub to_timestamp: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MessageMetadata {
pub id: String, pub entity_id: String, pub entity_type: EntityType, pub author_peer_id: String, pub vector_clock: VectorClock, pub lamport_clock: u64, pub timestamp: u64, pub previous_message_id: Option<String>, pub reply_to_id: Option<String>, }
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum EntityType {
Person,
Group,
Project,
Channel,
Organisation,
}
impl EntityType {
pub fn as_str(&self) -> &'static str {
match self {
EntityType::Person => "person",
EntityType::Group => "group",
EntityType::Project => "project",
EntityType::Channel => "channel",
EntityType::Organisation => "organisation",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CRDTMessage {
pub content: MessageContent,
pub metadata: MessageMetadata,
#[serde(skip_serializing_if = "Option::is_none")]
pub local_state: Option<LocalMessageState>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MessageContent {
pub text: String,
pub author: String, #[serde(skip_serializing_if = "Option::is_none")]
pub attachments: Option<Vec<Attachment>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Attachment {
pub attachment_type: AttachmentType,
pub url: String,
pub name: String,
pub size: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AttachmentType {
File,
Image,
Video,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct LocalMessageState {
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<MessageStatus>,
#[serde(default)]
pub reactions: Vec<Reaction>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edited_at: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thread_count: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub latest_reply_by: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum MessageStatus {
Sent,
Delivered,
Read,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Reaction {
pub emoji: String,
pub count: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_reacted: Option<bool>,
pub peer_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncRequest {
pub entity_id: String,
pub entity_type: EntityType,
pub requester_peer_id: String,
pub vector_clock: VectorClock,
#[serde(skip_serializing_if = "Option::is_none")]
pub missing_message_ids: Option<Vec<String>>,
#[serde(default)]
pub request_id: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncResponse {
pub entity_id: String,
pub entity_type: EntityType,
pub messages: Vec<CRDTMessage>,
pub vector_clock: VectorClock,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemberUpdate {
pub entity_id: String,
pub entity_type: EntityType,
pub member_id: String,
pub role: Option<String>,
pub updated_by: String,
pub action: MemberUpdateAction,
pub timestamp: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum MemberUpdateAction {
Add,
Remove,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemberSyncRequest {
pub entity_id: String,
pub entity_type: EntityType,
pub requester_peer_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemberSyncResponse {
pub entity_id: String,
pub entity_type: EntityType,
pub responder_peer_id: String,
pub updates: Vec<MemberUpdate>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntitySyncState {
pub entity_id: String,
pub entity_type: EntityType,
pub vector_clock: VectorClock,
pub last_sync_time: u64,
pub message_count: usize,
pub missing_messages: Vec<String>,
pub out_of_order_messages: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerInfo {
pub addr: String,
pub score: f64,
#[serde(default)]
pub nat_class: Option<String>,
#[serde(default)]
pub roles: Vec<String>,
}
impl PeerInfo {
pub fn new(addr: impl Into<String>) -> Self {
Self {
addr: addr.into(),
score: 0.5, nat_class: None,
roles: Vec::new(),
}
}
pub fn with_details(
addr: impl Into<String>,
score: f64,
nat_class: Option<String>,
roles: Vec<String>,
) -> Self {
Self {
addr: addr.into(),
score: score.clamp(0.0, 1.0),
nat_class,
roles,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerListRequest {
pub max_peers: u8,
#[serde(default)]
pub required_roles: Vec<String>,
#[serde(default)]
pub requester_nat_class: Option<String>,
}
impl PeerListRequest {
pub fn new(max_peers: u8) -> Self {
Self {
max_peers,
required_roles: Vec::new(),
requester_nat_class: None,
}
}
pub fn with_roles(max_peers: u8, roles: Vec<String>) -> Self {
Self {
max_peers,
required_roles: roles,
requester_nat_class: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerListResponse {
pub peers: Vec<PeerInfo>,
pub total_known_peers: usize,
}
impl PeerListResponse {
pub fn new(peers: Vec<PeerInfo>, total_known_peers: usize) -> Self {
Self {
peers,
total_known_peers,
}
}
pub fn empty() -> Self {
Self {
peers: Vec::new(),
total_known_peers: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CanvasOperationType {
Add,
Update,
Remove,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CanvasOperation {
pub operation_id: String,
pub canvas_id: String,
pub element_id: String,
pub operation_type: CanvasOperationType,
pub element_data: Option<serde_json::Value>,
pub vector_clock: VectorClock,
pub lamport_clock: u64,
pub origin_peer: String,
pub timestamp_ms: u64,
}
impl CanvasOperation {
pub fn add(
canvas_id: String,
element_id: String,
element_data: serde_json::Value,
vector_clock: VectorClock,
lamport_clock: u64,
origin_peer: String,
) -> Self {
Self {
operation_id: format!("{}-{}-add", canvas_id, element_id),
canvas_id,
element_id,
operation_type: CanvasOperationType::Add,
element_data: Some(element_data),
vector_clock,
lamport_clock,
origin_peer,
timestamp_ms: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0),
}
}
pub fn update(
canvas_id: String,
element_id: String,
element_data: serde_json::Value,
vector_clock: VectorClock,
lamport_clock: u64,
origin_peer: String,
) -> Self {
Self {
operation_id: format!("{}-{}-update-{}", canvas_id, element_id, lamport_clock),
canvas_id,
element_id,
operation_type: CanvasOperationType::Update,
element_data: Some(element_data),
vector_clock,
lamport_clock,
origin_peer,
timestamp_ms: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0),
}
}
pub fn remove(
canvas_id: String,
element_id: String,
vector_clock: VectorClock,
lamport_clock: u64,
origin_peer: String,
) -> Self {
Self {
operation_id: format!("{}-{}-remove", canvas_id, element_id),
canvas_id,
element_id,
operation_type: CanvasOperationType::Remove,
element_data: None,
vector_clock,
lamport_clock,
origin_peer,
timestamp_ms: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CanvasCursorUpdate {
pub canvas_id: String,
pub peer_id: String,
pub display_name: String,
pub x: f64,
pub y: f64,
pub hovered_element_id: Option<String>,
pub selected_elements: Vec<String>,
pub tool: Option<String>,
pub color: Option<String>,
pub timestamp_ms: u64,
}
impl CanvasCursorUpdate {
pub fn new(canvas_id: String, peer_id: String, display_name: String, x: f64, y: f64) -> Self {
Self {
canvas_id,
peer_id,
display_name,
x,
y,
hovered_element_id: None,
selected_elements: Vec::new(),
tool: None,
color: None,
timestamp_ms: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0),
}
}
pub fn with_hovered_element(mut self, element_id: String) -> Self {
self.hovered_element_id = Some(element_id);
self
}
pub fn with_selection(mut self, elements: Vec<String>) -> Self {
self.selected_elements = elements;
self
}
pub fn with_tool(mut self, tool: String) -> Self {
self.tool = Some(tool);
self
}
pub fn with_color(mut self, color: String) -> Self {
self.color = Some(color);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CanvasStateRequest {
pub canvas_id: String,
pub requester_peer_id: String,
pub since_vector_clock: Option<VectorClock>,
}
impl CanvasStateRequest {
pub fn full(canvas_id: String, requester_peer_id: String) -> Self {
Self {
canvas_id,
requester_peer_id,
since_vector_clock: None,
}
}
pub fn incremental(canvas_id: String, requester_peer_id: String, since: VectorClock) -> Self {
Self {
canvas_id,
requester_peer_id,
since_vector_clock: Some(since),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CanvasStateResponse {
pub canvas_id: String,
pub responder_peer_id: String,
pub elements: Vec<serde_json::Value>,
pub vector_clock: VectorClock,
pub is_incremental: bool,
pub total_element_count: usize,
}
impl CanvasStateResponse {
pub fn full(
canvas_id: String,
responder_peer_id: String,
elements: Vec<serde_json::Value>,
vector_clock: VectorClock,
) -> Self {
let total = elements.len();
Self {
canvas_id,
responder_peer_id,
elements,
vector_clock,
is_incremental: false,
total_element_count: total,
}
}
pub fn incremental(
canvas_id: String,
responder_peer_id: String,
elements: Vec<serde_json::Value>,
vector_clock: VectorClock,
total_element_count: usize,
) -> Self {
Self {
canvas_id,
responder_peer_id,
elements,
vector_clock,
is_incremental: true,
total_element_count,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[allow(clippy::large_enum_variant)]
pub enum GossipMessageType {
Chat(CRDTMessage),
SyncRequest(SyncRequest),
SyncResponse(SyncResponse),
MemberUpdate(MemberUpdate),
MemberSyncRequest(MemberSyncRequest),
MemberSyncResponse(MemberSyncResponse),
PeerListRequest(PeerListRequest),
PeerListResponse(PeerListResponse),
CanvasOperation(CanvasOperation),
CanvasCursorUpdate(CanvasCursorUpdate),
CanvasStateRequest(CanvasStateRequest),
CanvasStateResponse(CanvasStateResponse),
}
pub fn sort_messages_causally(messages: &mut [CRDTMessage]) {
messages.sort_by(|a, b| {
match a.metadata.vector_clock.compare(&b.metadata.vector_clock) {
ClockOrdering::Before => Ordering::Less,
ClockOrdering::After => Ordering::Greater,
ClockOrdering::Equal | ClockOrdering::Concurrent => {
match a.metadata.lamport_clock.cmp(&b.metadata.lamport_clock) {
Ordering::Equal => {
a.metadata.id.cmp(&b.metadata.id)
}
other => other,
}
}
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vector_clock_increment() {
let mut clock = VectorClock::new();
clock.increment("alice");
clock.increment("alice");
clock.increment("bob");
assert_eq!(clock.0.get("alice"), Some(&2));
assert_eq!(clock.0.get("bob"), Some(&1));
}
#[test]
fn test_vector_clock_comparison() {
let mut clock1 = VectorClock::new();
clock1.increment("alice");
clock1.increment("alice");
let mut clock2 = VectorClock::new();
clock2.increment("alice");
assert_eq!(clock1.compare(&clock2), ClockOrdering::After);
assert_eq!(clock2.compare(&clock1), ClockOrdering::Before);
let mut clock3 = VectorClock::new();
clock3.increment("bob");
assert_eq!(clock1.compare(&clock3), ClockOrdering::Concurrent);
}
#[test]
fn test_vector_clock_merge() {
let mut clock1 = VectorClock::new();
clock1.increment("alice");
clock1.increment("alice");
let mut clock2 = VectorClock::new();
clock2.increment("alice");
clock2.increment("bob");
clock1.merge(&clock2);
assert_eq!(clock1.0.get("alice"), Some(&2));
assert_eq!(clock1.0.get("bob"), Some(&1));
}
#[test]
fn test_has_dependencies() {
let mut local = VectorClock::new();
local.increment("alice");
local.increment("alice");
let mut message_clock = VectorClock::new();
message_clock.increment("alice");
message_clock.increment("alice");
message_clock.increment("alice");
assert!(local.has_dependencies(&message_clock));
message_clock.increment("alice");
message_clock.increment("alice");
assert!(!local.has_dependencies(&message_clock));
}
#[test]
fn test_canvas_operation_add_serialization() {
let mut clock = VectorClock::new();
clock.increment("peer-1");
let op = CanvasOperation::add(
"canvas-123".to_string(),
"element-456".to_string(),
serde_json::json!({"type": "rectangle", "x": 100, "y": 200}),
clock,
1,
"peer-1".to_string(),
);
let json = serde_json::to_string(&op).unwrap();
let deserialized: CanvasOperation = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.canvas_id, "canvas-123");
assert_eq!(deserialized.element_id, "element-456");
assert_eq!(deserialized.operation_type, CanvasOperationType::Add);
assert!(deserialized.element_data.is_some());
}
#[test]
fn test_canvas_operation_remove_serialization() {
let clock = VectorClock::new();
let op = CanvasOperation::remove(
"canvas-123".to_string(),
"element-456".to_string(),
clock,
5,
"peer-2".to_string(),
);
let json = serde_json::to_string(&op).unwrap();
let deserialized: CanvasOperation = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.operation_type, CanvasOperationType::Remove);
assert!(deserialized.element_data.is_none());
}
#[test]
fn test_canvas_cursor_update_serialization() {
let cursor = CanvasCursorUpdate::new(
"canvas-123".to_string(),
"peer-1".to_string(),
"Alice".to_string(),
150.5,
200.75,
)
.with_hovered_element("elem-1".to_string())
.with_selection(vec!["elem-2".to_string(), "elem-3".to_string()]);
let json = serde_json::to_string(&cursor).unwrap();
let deserialized: CanvasCursorUpdate = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.canvas_id, "canvas-123");
assert_eq!(deserialized.peer_id, "peer-1");
assert_eq!(deserialized.display_name, "Alice");
assert!((deserialized.x - 150.5).abs() < f64::EPSILON);
assert!((deserialized.y - 200.75).abs() < f64::EPSILON);
assert_eq!(deserialized.hovered_element_id, Some("elem-1".to_string()));
assert_eq!(deserialized.selected_elements.len(), 2);
}
#[test]
fn test_canvas_state_request_serialization() {
let request = CanvasStateRequest::full("canvas-123".to_string(), "peer-1".to_string());
let json = serde_json::to_string(&request).unwrap();
let deserialized: CanvasStateRequest = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.canvas_id, "canvas-123");
assert_eq!(deserialized.requester_peer_id, "peer-1");
assert!(deserialized.since_vector_clock.is_none());
let mut clock = VectorClock::new();
clock.increment("peer-1");
let incremental =
CanvasStateRequest::incremental("canvas-456".to_string(), "peer-2".to_string(), clock);
let json = serde_json::to_string(&incremental).unwrap();
let deserialized: CanvasStateRequest = serde_json::from_str(&json).unwrap();
assert!(deserialized.since_vector_clock.is_some());
}
#[test]
fn test_canvas_state_response_serialization() {
let mut clock = VectorClock::new();
clock.increment("peer-1");
clock.increment("peer-2");
let elements = vec![
serde_json::json!({"id": "elem-1", "type": "rectangle"}),
serde_json::json!({"id": "elem-2", "type": "circle"}),
];
let response = CanvasStateResponse::full(
"canvas-123".to_string(),
"peer-1".to_string(),
elements,
clock,
);
let json = serde_json::to_string(&response).unwrap();
let deserialized: CanvasStateResponse = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.canvas_id, "canvas-123");
assert_eq!(deserialized.responder_peer_id, "peer-1");
assert_eq!(deserialized.elements.len(), 2);
assert_eq!(deserialized.total_element_count, 2);
assert!(!deserialized.is_incremental);
}
#[test]
fn test_gossip_message_canvas_operation_roundtrip() {
let mut clock = VectorClock::new();
clock.increment("peer-1");
let op = CanvasOperation::update(
"canvas-123".to_string(),
"element-456".to_string(),
serde_json::json!({"x": 300, "y": 400}),
clock,
10,
"peer-1".to_string(),
);
let msg = GossipMessageType::CanvasOperation(op);
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"type\":\"canvas_operation\""));
let deserialized: GossipMessageType = serde_json::from_str(&json).unwrap();
match deserialized {
GossipMessageType::CanvasOperation(op) => {
assert_eq!(op.canvas_id, "canvas-123");
assert_eq!(op.operation_type, CanvasOperationType::Update);
}
_ => panic!("Expected CanvasOperation variant"),
}
}
#[test]
fn test_gossip_message_canvas_cursor_roundtrip() {
let cursor = CanvasCursorUpdate::new(
"canvas-123".to_string(),
"peer-1".to_string(),
"Bob".to_string(),
50.0,
75.0,
);
let msg = GossipMessageType::CanvasCursorUpdate(cursor);
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"type\":\"canvas_cursor_update\""));
let deserialized: GossipMessageType = serde_json::from_str(&json).unwrap();
match deserialized {
GossipMessageType::CanvasCursorUpdate(c) => {
assert_eq!(c.display_name, "Bob");
}
_ => panic!("Expected CanvasCursorUpdate variant"),
}
}
#[test]
fn test_gossip_message_canvas_state_request_roundtrip() {
let request = CanvasStateRequest::full("canvas-789".to_string(), "peer-3".to_string());
let msg = GossipMessageType::CanvasStateRequest(request);
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"type\":\"canvas_state_request\""));
let deserialized: GossipMessageType = serde_json::from_str(&json).unwrap();
match deserialized {
GossipMessageType::CanvasStateRequest(r) => {
assert_eq!(r.canvas_id, "canvas-789");
}
_ => panic!("Expected CanvasStateRequest variant"),
}
}
#[test]
fn test_gossip_message_canvas_state_response_roundtrip() {
let clock = VectorClock::new();
let response = CanvasStateResponse::incremental(
"canvas-123".to_string(),
"peer-1".to_string(),
vec![serde_json::json!({"id": "new-elem"})],
clock,
100,
);
let msg = GossipMessageType::CanvasStateResponse(response);
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"type\":\"canvas_state_response\""));
let deserialized: GossipMessageType = serde_json::from_str(&json).unwrap();
match deserialized {
GossipMessageType::CanvasStateResponse(r) => {
assert!(r.is_incremental);
assert_eq!(r.total_element_count, 100);
}
_ => panic!("Expected CanvasStateResponse variant"),
}
}
}