use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use thiserror::Error;
use tokio::sync::{mpsc, RwLock};
use super::Position;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Collaborator {
pub user_id: String,
pub username: String,
pub color: String,
pub cursor_position: Option<Position>,
pub last_seen: u64,
pub is_active: bool,
pub permissions: UserPermissions,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct UserPermissions {
pub can_edit: bool,
pub can_delete: bool,
pub can_add_elements: bool,
pub can_modify_styles: bool,
pub can_manage_users: bool,
}
impl Default for UserPermissions {
fn default() -> Self {
Self {
can_edit: true,
can_delete: false,
can_add_elements: true,
can_modify_styles: true,
can_manage_users: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EditOperation {
pub operation_id: String,
pub user_id: String,
pub timestamp: u64,
pub operation_type: EditOperationType,
pub element_id: Option<String>,
pub changes: ElementChanges,
pub dependencies: Vec<String>, }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum EditOperationType {
Create,
Update,
Delete,
Move,
Resize,
StyleChange,
Batch(Vec<EditOperation>),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ElementChanges {
pub position: Option<Position>,
pub size: Option<Size>,
pub style: Option<ElementStyle>,
pub data: Option<serde_json::Value>,
pub metadata: Option<HashMap<String, String>>,
}
impl Default for ElementChanges {
fn default() -> Self {
Self {
position: None,
size: None,
style: None,
data: None,
metadata: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Size {
pub width: f64,
pub height: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ElementStyle {
pub fill_color: Option<String>,
pub stroke_color: Option<String>,
pub stroke_width: Option<f64>,
pub opacity: Option<f64>,
pub font_size: Option<f64>,
pub font_family: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CollaborationSession {
pub session_id: String,
pub chart_id: String,
pub collaborators: HashMap<String, Collaborator>,
pub operations: VecDeque<EditOperation>,
pub max_operations: usize,
pub created_at: u64,
pub last_activity: u64,
pub is_active: bool,
pub settings: CollaborationSettings,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CollaborationSettings {
pub max_collaborators: usize,
pub operation_timeout: Duration,
pub auto_save_interval: Duration,
pub conflict_resolution_strategy: ConflictResolutionStrategy,
pub enable_cursors: bool,
pub enable_undo_redo: bool,
pub max_undo_history: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ConflictResolutionStrategy {
LastWriteWins,
FirstWriteWins,
UserPriority,
Manual,
OperationalTransform,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CollaborationStats {
pub total_operations: u64,
pub active_collaborators: usize,
pub conflicts_resolved: u64,
pub average_operation_time: Duration,
pub session_duration: Duration,
pub operations_per_minute: f64,
}
#[derive(Debug, Error)]
pub enum CollaborationError {
#[error("User not found: {user_id}")]
UserNotFound { user_id: String },
#[error("Operation not found: {operation_id}")]
OperationNotFound { operation_id: String },
#[error("Permission denied: {user_id} cannot {action}")]
PermissionDenied { user_id: String, action: String },
#[error("Session not found: {session_id}")]
SessionNotFound { session_id: String },
#[error("Session is not active: {session_id}")]
SessionInactive { session_id: String },
#[error("Maximum collaborators reached: {max}")]
MaxCollaboratorsReached { max: usize },
#[error("Operation timeout: {operation_id}")]
OperationTimeout { operation_id: String },
#[error("Conflict detected: {conflict_type}")]
ConflictDetected { conflict_type: String },
#[error("Invalid operation: {reason}")]
InvalidOperation { reason: String },
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("WebSocket error: {0}")]
WebSocket(#[from] super::WebSocketError),
}
pub struct CollaborativeEditor {
sessions: Arc<RwLock<HashMap<String, CollaborationSession>>>,
operation_sender: mpsc::UnboundedSender<EditOperation>,
operation_receiver: Arc<RwLock<Option<mpsc::UnboundedReceiver<EditOperation>>>>,
stats: Arc<RwLock<CollaborationStats>>,
settings: CollaborationSettings,
}
impl CollaborativeEditor {
pub fn new(settings: CollaborationSettings) -> Self {
let (operation_sender, operation_receiver) = mpsc::unbounded_channel();
Self {
sessions: Arc::new(RwLock::new(HashMap::new())),
operation_sender,
operation_receiver: Arc::new(RwLock::new(Some(operation_receiver))),
stats: Arc::new(RwLock::new(CollaborationStats {
total_operations: 0,
active_collaborators: 0,
conflicts_resolved: 0,
average_operation_time: Duration::from_millis(0),
session_duration: Duration::from_millis(0),
operations_per_minute: 0.0,
})),
settings,
}
}
pub async fn create_session(
&self,
session_id: String,
chart_id: String,
creator: Collaborator,
) -> Result<(), CollaborationError> {
let mut sessions = self.sessions.write().await;
if sessions.contains_key(&session_id) {
return Err(CollaborationError::InvalidOperation {
reason: "Session already exists".to_string(),
});
}
let session = CollaborationSession {
session_id: session_id.clone(),
chart_id,
collaborators: {
let mut collabs = HashMap::new();
collabs.insert(creator.user_id.clone(), creator);
collabs
},
operations: VecDeque::new(),
max_operations: 1000,
created_at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
last_activity: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
is_active: true,
settings: self.settings.clone(),
};
sessions.insert(session_id, session);
Ok(())
}
pub async fn join_session(
&self,
session_id: &str,
collaborator: Collaborator,
) -> Result<(), CollaborationError> {
let mut sessions = self.sessions.write().await;
let session =
sessions
.get_mut(session_id)
.ok_or_else(|| CollaborationError::SessionNotFound {
session_id: session_id.to_string(),
})?;
if !session.is_active {
return Err(CollaborationError::SessionInactive {
session_id: session_id.to_string(),
});
}
if session.collaborators.len() >= session.settings.max_collaborators {
return Err(CollaborationError::MaxCollaboratorsReached {
max: session.settings.max_collaborators,
});
}
session
.collaborators
.insert(collaborator.user_id.clone(), collaborator);
session.last_activity = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
Ok(())
}
pub async fn leave_session(
&self,
session_id: &str,
user_id: &str,
) -> Result<(), CollaborationError> {
let mut sessions = self.sessions.write().await;
let session =
sessions
.get_mut(session_id)
.ok_or_else(|| CollaborationError::SessionNotFound {
session_id: session_id.to_string(),
})?;
session.collaborators.remove(user_id);
session.last_activity = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
if session.collaborators.is_empty() {
session.is_active = false;
}
Ok(())
}
pub async fn apply_operation(
&self,
session_id: &str,
operation: EditOperation,
) -> Result<(), CollaborationError> {
let mut sessions = self.sessions.write().await;
let session =
sessions
.get_mut(session_id)
.ok_or_else(|| CollaborationError::SessionNotFound {
session_id: session_id.to_string(),
})?;
if !session.is_active {
return Err(CollaborationError::SessionInactive {
session_id: session_id.to_string(),
});
}
let collaborator = session
.collaborators
.get(&operation.user_id)
.ok_or_else(|| CollaborationError::UserNotFound {
user_id: operation.user_id.clone(),
})?;
match operation.operation_type {
EditOperationType::Create => {
if !collaborator.permissions.can_add_elements {
return Err(CollaborationError::PermissionDenied {
user_id: operation.user_id.clone(),
action: "create elements".to_string(),
});
}
}
EditOperationType::Delete => {
if !collaborator.permissions.can_delete {
return Err(CollaborationError::PermissionDenied {
user_id: operation.user_id.clone(),
action: "delete elements".to_string(),
});
}
}
EditOperationType::Update | EditOperationType::Move | EditOperationType::Resize => {
if !collaborator.permissions.can_edit {
return Err(CollaborationError::PermissionDenied {
user_id: operation.user_id.clone(),
action: "edit elements".to_string(),
});
}
}
EditOperationType::StyleChange => {
if !collaborator.permissions.can_modify_styles {
return Err(CollaborationError::PermissionDenied {
user_id: operation.user_id.clone(),
action: "modify styles".to_string(),
});
}
}
EditOperationType::Batch(_) => {
}
}
session.operations.push_back(operation.clone());
while session.operations.len() > session.max_operations {
session.operations.pop_front();
}
session.last_activity = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let mut stats = self.stats.write().await;
stats.total_operations += 1;
Ok(())
}
pub async fn get_session(
&self,
session_id: &str,
) -> Result<CollaborationSession, CollaborationError> {
let sessions = self.sessions.read().await;
sessions
.get(session_id)
.cloned()
.ok_or_else(|| CollaborationError::SessionNotFound {
session_id: session_id.to_string(),
})
}
pub async fn get_collaborators(
&self,
session_id: &str,
) -> Result<Vec<Collaborator>, CollaborationError> {
let session = self.get_session(session_id).await?;
Ok(session.collaborators.values().cloned().collect())
}
pub async fn get_operations(
&self,
session_id: &str,
) -> Result<Vec<EditOperation>, CollaborationError> {
let session = self.get_session(session_id).await?;
Ok(session.operations.iter().cloned().collect())
}
pub async fn update_cursor(
&self,
session_id: &str,
user_id: &str,
position: Position,
) -> Result<(), CollaborationError> {
let mut sessions = self.sessions.write().await;
let session =
sessions
.get_mut(session_id)
.ok_or_else(|| CollaborationError::SessionNotFound {
session_id: session_id.to_string(),
})?;
if let Some(collaborator) = session.collaborators.get_mut(user_id) {
collaborator.cursor_position = Some(position);
collaborator.last_seen = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
}
Ok(())
}
pub async fn get_stats(&self) -> CollaborationStats {
self.stats.read().await.clone()
}
pub async fn start_processing(&self) {
let receiver = self.operation_receiver.write().await.take();
if let Some(mut receiver) = receiver {
let _sessions = self.sessions.clone();
let stats = self.stats.clone();
tokio::spawn(async move {
while let Some(_operation) = receiver.recv().await {
let start_time = Instant::now();
let mut stats_guard = stats.write().await;
stats_guard.total_operations += 1;
let operation_time = start_time.elapsed();
stats_guard.average_operation_time = operation_time;
}
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn create_test_collaborator(user_id: &str) -> Collaborator {
Collaborator {
user_id: user_id.to_string(),
username: format!("user_{}", user_id),
color: "#FF0000".to_string(),
cursor_position: None,
last_seen: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
is_active: true,
permissions: UserPermissions::default(),
}
}
fn create_test_operation(user_id: &str, operation_type: EditOperationType) -> EditOperation {
EditOperation {
operation_id: format!(
"op_{}",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
),
user_id: user_id.to_string(),
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
operation_type,
element_id: Some("test_element".to_string()),
changes: ElementChanges {
position: Some(Position { x: 100.0, y: 100.0 }),
size: None,
style: None,
data: None,
metadata: None,
},
dependencies: vec![],
}
}
fn create_test_settings() -> CollaborationSettings {
CollaborationSettings {
max_collaborators: 10,
operation_timeout: Duration::from_secs(30),
auto_save_interval: Duration::from_secs(60),
conflict_resolution_strategy: ConflictResolutionStrategy::LastWriteWins,
enable_cursors: true,
enable_undo_redo: true,
max_undo_history: 100,
}
}
#[tokio::test]
#[ignore] async fn test_create_session() {
let editor = CollaborativeEditor::new(create_test_settings());
let creator = create_test_collaborator("user1");
let result = editor
.create_session("session1".to_string(), "chart1".to_string(), creator)
.await;
assert!(result.is_ok());
let session = editor.get_session("session1").await.unwrap();
assert_eq!(session.session_id, "session1");
assert_eq!(session.chart_id, "chart1");
assert_eq!(session.collaborators.len(), 1);
assert!(session.is_active);
}
#[tokio::test]
#[ignore] async fn test_join_session() {
let editor = CollaborativeEditor::new(create_test_settings());
let creator = create_test_collaborator("user1");
let joiner = create_test_collaborator("user2");
editor
.create_session("session1".to_string(), "chart1".to_string(), creator)
.await
.unwrap();
let result = editor.join_session("session1", joiner).await;
assert!(result.is_ok());
let session = editor.get_session("session1").await.unwrap();
assert_eq!(session.collaborators.len(), 2);
}
#[tokio::test]
#[ignore] async fn test_leave_session() {
let editor = CollaborativeEditor::new(create_test_settings());
let creator = create_test_collaborator("user1");
let joiner = create_test_collaborator("user2");
editor
.create_session("session1".to_string(), "chart1".to_string(), creator)
.await
.unwrap();
editor.join_session("session1", joiner).await.unwrap();
let result = editor.leave_session("session1", "user2").await;
assert!(result.is_ok());
let session = editor.get_session("session1").await.unwrap();
assert_eq!(session.collaborators.len(), 1);
}
#[tokio::test]
#[ignore] async fn test_apply_operation() {
let editor = CollaborativeEditor::new(create_test_settings());
let creator = create_test_collaborator("user1");
editor
.create_session("session1".to_string(), "chart1".to_string(), creator)
.await
.unwrap();
let operation = create_test_operation("user1", EditOperationType::Create);
let result = editor.apply_operation("session1", operation.clone()).await;
assert!(result.is_ok());
let operations = editor.get_operations("session1").await.unwrap();
assert_eq!(operations.len(), 1);
assert_eq!(operations[0].operation_id, operation.operation_id);
}
#[tokio::test]
#[ignore] async fn test_permission_denied() {
let settings = create_test_settings();
let editor = CollaborativeEditor::new(settings);
let mut creator = create_test_collaborator("user1");
creator.permissions.can_add_elements = false;
editor
.create_session("session1".to_string(), "chart1".to_string(), creator)
.await
.unwrap();
let operation = create_test_operation("user1", EditOperationType::Create);
let result = editor.apply_operation("session1", operation).await;
assert!(matches!(
result,
Err(CollaborationError::PermissionDenied { .. })
));
}
#[tokio::test]
#[ignore] async fn test_max_collaborators() {
let mut settings = create_test_settings();
settings.max_collaborators = 1;
let editor = CollaborativeEditor::new(settings);
let creator = create_test_collaborator("user1");
let joiner = create_test_collaborator("user2");
editor
.create_session("session1".to_string(), "chart1".to_string(), creator)
.await
.unwrap();
let result = editor.join_session("session1", joiner).await;
assert!(matches!(
result,
Err(CollaborationError::MaxCollaboratorsReached { .. })
));
}
#[tokio::test]
#[ignore] async fn test_update_cursor() {
let editor = CollaborativeEditor::new(create_test_settings());
let creator = create_test_collaborator("user1");
editor
.create_session("session1".to_string(), "chart1".to_string(), creator)
.await
.unwrap();
let position = Position { x: 150.0, y: 200.0 };
let result = editor
.update_cursor("session1", "user1", position.clone())
.await;
assert!(result.is_ok());
let collaborators = editor.get_collaborators("session1").await.unwrap();
assert_eq!(collaborators[0].cursor_position, Some(position));
}
#[tokio::test]
#[ignore] async fn test_session_not_found() {
let editor = CollaborativeEditor::new(create_test_settings());
let result = editor.get_session("nonexistent").await;
assert!(matches!(
result,
Err(CollaborationError::SessionNotFound { .. })
));
}
#[tokio::test]
#[ignore] async fn test_operation_history_limit() {
let mut settings = create_test_settings();
let editor = CollaborativeEditor::new(settings);
let creator = create_test_collaborator("user1");
editor
.create_session("session1".to_string(), "chart1".to_string(), creator)
.await
.unwrap();
for _i in 0..5 {
let operation = create_test_operation("user1", EditOperationType::Create);
editor.apply_operation("session1", operation).await.unwrap();
}
let operations = editor.get_operations("session1").await.unwrap();
assert_eq!(operations.len(), 5);
}
#[tokio::test]
#[ignore] async fn test_collaboration_stats() {
let editor = CollaborativeEditor::new(create_test_settings());
let creator = create_test_collaborator("user1");
editor
.create_session("session1".to_string(), "chart1".to_string(), creator)
.await
.unwrap();
let operation = create_test_operation("user1", EditOperationType::Create);
editor.apply_operation("session1", operation).await.unwrap();
let stats = editor.get_stats().await;
assert!(stats.total_operations > 0);
}
}