use serde_json;
use crate::canvas::CanvasDocument;
use crate::crdt::CrdtDocument;
use crate::shapes::{Shape, ShapeId};
use crate::sync::{
AwarenessState, ClientMessage, CursorPosition, ServerMessage, SyncEvent,
base64_decode, base64_encode,
};
pub struct CollaborationManager {
crdt: CrdtDocument,
enabled: bool,
peer_id: u64,
current_room: Option<String>,
awareness: AwarenessState,
outgoing: Vec<String>,
}
impl CollaborationManager {
pub fn new() -> Self {
let crdt = CrdtDocument::new();
let peer_id = crdt.loro_doc().peer_id();
Self {
crdt,
enabled: false,
peer_id,
current_room: None,
awareness: AwarenessState::default(),
outgoing: Vec::new(),
}
}
pub fn from_crdt(crdt: CrdtDocument) -> Self {
let peer_id = crdt.loro_doc().peer_id();
Self {
crdt,
enabled: false,
peer_id,
current_room: None,
awareness: AwarenessState::default(),
outgoing: Vec::new(),
}
}
pub fn peer_id(&self) -> u64 {
self.peer_id
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn enable(&mut self) {
self.enabled = true;
}
pub fn disable(&mut self) {
self.enabled = false;
}
pub fn crdt(&self) -> &CrdtDocument {
&self.crdt
}
pub fn crdt_mut(&mut self) -> &mut CrdtDocument {
&mut self.crdt
}
pub fn sync_to_crdt(&mut self, doc: &CanvasDocument) {
if !self.enabled {
return;
}
let _ = self.crdt.clear();
let _ = self.crdt.set_name(&doc.name);
for shape_id in &doc.z_order {
if let Some(shape) = doc.shapes.get(shape_id) {
let _ = self.crdt.add_shape(shape);
}
}
}
pub fn sync_from_crdt(&self, doc: &mut CanvasDocument) {
doc.shapes.clear();
doc.z_order.clear();
let name = self.crdt.name();
if !name.is_empty() {
doc.name = name;
}
let z_order = self.crdt.z_order();
for id_str in z_order {
if let Some(shape) = self.crdt.get_shape(&id_str) {
let shape_id = shape.id();
doc.shapes.insert(shape_id, shape);
doc.z_order.push(shape_id);
}
}
}
pub fn add_shape(&mut self, doc: &mut CanvasDocument, shape: Shape) {
let shape_clone = shape.clone();
doc.add_shape(shape);
if self.enabled {
let _ = self.crdt.add_shape(&shape_clone);
}
}
pub fn remove_shape(&mut self, doc: &mut CanvasDocument, id: ShapeId) -> Option<Shape> {
let result = doc.remove_shape(id);
if self.enabled && result.is_some() {
let _ = self.crdt.remove_shape(&id.to_string());
}
result
}
pub fn update_shape(&mut self, doc: &mut CanvasDocument, shape: Shape) {
let id = shape.id();
if let Some(existing) = doc.shapes.get_mut(&id) {
*existing = shape.clone();
if self.enabled {
let _ = self.crdt.update_shape(&shape);
}
}
}
pub fn export_snapshot(&self) -> Vec<u8> {
self.crdt.export_snapshot()
}
pub fn export_updates(&self, since: &loro::VersionVector) -> Vec<u8> {
self.crdt.export_updates(since)
}
pub fn import_updates(&mut self, bytes: &[u8]) -> bool {
self.crdt.import(bytes).is_ok()
}
pub fn version(&self) -> loro::VersionVector {
self.crdt.version()
}
pub fn undo(&mut self) -> bool {
if self.enabled {
self.crdt.undo()
} else {
false
}
}
pub fn redo(&mut self) -> bool {
if self.enabled {
self.crdt.redo()
} else {
false
}
}
pub fn can_undo(&self) -> bool {
if self.enabled {
self.crdt.can_undo()
} else {
false
}
}
pub fn can_redo(&self) -> bool {
if self.enabled {
self.crdt.can_redo()
} else {
false
}
}
pub fn current_room(&self) -> Option<&str> {
self.current_room.as_deref()
}
pub fn is_in_room(&self) -> bool {
self.current_room.is_some()
}
pub fn set_room(&mut self, room: Option<String>) {
self.current_room = room;
if self.current_room.is_some() {
self.enable();
}
}
pub fn join_room(&mut self, room: &str) {
let msg = ClientMessage::Join { room: room.to_string() };
if let Ok(json) = serde_json::to_string(&msg) {
self.outgoing.push(json);
}
}
pub fn leave_room(&mut self) {
if self.current_room.is_some() {
let msg = ClientMessage::Leave;
if let Ok(json) = serde_json::to_string(&msg) {
self.outgoing.push(json);
}
self.current_room = None;
}
}
pub fn take_outgoing(&mut self) -> Vec<String> {
std::mem::take(&mut self.outgoing)
}
pub fn has_outgoing(&self) -> bool {
!self.outgoing.is_empty()
}
pub fn set_cursor(&mut self, x: f64, y: f64) {
self.awareness.cursor = Some(CursorPosition { x, y });
self.queue_awareness();
}
pub fn clear_cursor(&mut self) {
self.awareness.cursor = None;
self.queue_awareness();
}
pub fn set_user_info(&mut self, name: String, color: String) {
self.awareness.user = Some(crate::sync::UserInfo { name, color });
self.queue_awareness();
}
pub fn awareness(&self) -> &AwarenessState {
&self.awareness
}
fn queue_awareness(&mut self) {
if self.current_room.is_some() {
let msg = ClientMessage::Awareness {
peer_id: self.peer_id,
state: self.awareness.clone(),
};
if let Ok(json) = serde_json::to_string(&msg) {
self.outgoing.push(json);
}
}
}
pub fn broadcast_sync(&mut self) {
if self.current_room.is_some() && self.enabled {
let snapshot = self.crdt.export_snapshot();
let data = base64_encode(&snapshot);
let msg = ClientMessage::Sync { data };
if let Ok(json) = serde_json::to_string(&msg) {
self.outgoing.push(json);
}
}
}
pub fn handle_message(&mut self, json: &str) -> Option<SyncEvent> {
let msg: ServerMessage = serde_json::from_str(json).ok()?;
match msg {
ServerMessage::Joined { room, peer_count, initial_sync } => {
self.current_room = Some(room.clone());
self.enable();
let initial_data = initial_sync.and_then(|s| {
base64_decode(&s).and_then(|bytes| {
if self.crdt.import(&bytes).is_ok() {
Some(bytes)
} else {
None
}
})
});
Some(SyncEvent::JoinedRoom {
room,
peer_count,
initial_sync: initial_data,
})
}
ServerMessage::PeerJoined { peer_id } => {
Some(SyncEvent::PeerJoined { peer_id })
}
ServerMessage::PeerLeft { peer_id } => {
Some(SyncEvent::PeerLeft { peer_id })
}
ServerMessage::Sync { from, data } => {
if let Some(bytes) = base64_decode(&data) {
if self.crdt.import(&bytes).is_ok() {
return Some(SyncEvent::SyncReceived { from, data: bytes });
}
}
None
}
ServerMessage::Awareness { from, peer_id, state } => {
Some(SyncEvent::AwarenessReceived { from, peer_id, state })
}
ServerMessage::Error { message } => {
Some(SyncEvent::Error { message })
}
}
}
}
impl Default for CollaborationManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shapes::Rectangle;
use kurbo::Point;
#[test]
fn test_collaboration_disabled_by_default() {
let manager = CollaborationManager::new();
assert!(!manager.is_enabled());
}
#[test]
fn test_sync_to_crdt() {
let mut manager = CollaborationManager::new();
manager.enable();
let mut doc = CanvasDocument::new();
let rect = Rectangle::new(Point::new(0.0, 0.0), 100.0, 50.0);
doc.add_shape(Shape::Rectangle(rect));
manager.sync_to_crdt(&doc);
assert_eq!(manager.crdt().shape_count(), 1);
}
#[test]
fn test_sync_from_crdt() {
let mut manager = CollaborationManager::new();
let rect = Rectangle::new(Point::new(10.0, 20.0), 100.0, 50.0);
manager.crdt_mut().add_shape(&Shape::Rectangle(rect)).unwrap();
let mut doc = CanvasDocument::new();
manager.sync_from_crdt(&mut doc);
assert_eq!(doc.shapes.len(), 1);
assert_eq!(doc.z_order.len(), 1);
}
#[test]
fn test_export_import_snapshot() {
let mut manager1 = CollaborationManager::new();
manager1.enable();
let rect = Rectangle::new(Point::new(50.0, 50.0), 200.0, 100.0);
manager1.crdt_mut().add_shape(&Shape::Rectangle(rect)).unwrap();
let snapshot = manager1.export_snapshot();
let mut manager2 = CollaborationManager::new();
assert!(manager2.import_updates(&snapshot));
assert_eq!(manager2.crdt().shape_count(), 1);
}
#[test]
fn test_add_shape_with_sync() {
let mut manager = CollaborationManager::new();
manager.enable();
let mut doc = CanvasDocument::new();
let rect = Rectangle::new(Point::new(0.0, 0.0), 100.0, 100.0);
manager.add_shape(&mut doc, Shape::Rectangle(rect));
assert_eq!(doc.shapes.len(), 1);
assert_eq!(manager.crdt().shape_count(), 1);
}
#[test]
fn test_remove_shape_with_sync() {
let mut manager = CollaborationManager::new();
manager.enable();
let mut doc = CanvasDocument::new();
let rect = Rectangle::new(Point::new(0.0, 0.0), 100.0, 100.0);
let shape = Shape::Rectangle(rect);
let id = shape.id();
manager.add_shape(&mut doc, shape);
assert_eq!(doc.shapes.len(), 1);
assert_eq!(manager.crdt().shape_count(), 1);
manager.remove_shape(&mut doc, id);
assert_eq!(doc.shapes.len(), 0);
assert_eq!(manager.crdt().shape_count(), 0);
}
}