use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SyncEvent {
SettingsChanged {
version: u64,
changes: Vec<SettingsChange>,
device_id: String,
},
FullSyncRequested { reason: String },
ConflictDetected {
path: String,
local_value: serde_json::Value,
remote_value: serde_json::Value,
local_timestamp: DateTime<Utc>,
remote_timestamp: DateTime<Utc>,
},
SessionInvalidated { device_id: String, reason: String },
ApiKeyRotated { key_id: Uuid },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingsChange {
pub path: String,
pub operation: ChangeOperation,
pub value: Option<serde_json::Value>,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ChangeOperation {
Set,
Delete,
Merge,
}
#[derive(Debug, Clone)]
pub struct SyncConfig {
pub ping_interval_secs: u64,
pub connection_timeout_secs: u64,
pub max_offline_queue: usize,
pub retry_delay_secs: u64,
pub max_retries: u32,
}
impl Default for SyncConfig {
fn default() -> Self {
Self {
ping_interval_secs: 30,
connection_timeout_secs: 60,
max_offline_queue: 100,
retry_delay_secs: 5,
max_retries: 3,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceSession {
pub device_id: String,
pub device_name: String,
pub platform: String,
pub last_active: DateTime<Utc>,
pub settings_version: u64,
pub connected: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConflictResolution {
UseLocal,
UseRemote,
UseNewest,
Merge,
Manual,
}
pub struct SyncService {
#[allow(dead_code)]
config: SyncConfig,
sessions: HashMap<String, DeviceSession>,
}
impl SyncService {
pub fn new(config: SyncConfig) -> Self {
Self {
config,
sessions: HashMap::new(),
}
}
pub fn register_session(&mut self, session: DeviceSession) -> Result<(), SyncError> {
self.sessions.insert(session.device_id.clone(), session);
Ok(())
}
pub fn remove_session(&mut self, device_id: &str) -> Result<(), SyncError> {
self.sessions.remove(device_id);
Ok(())
}
pub fn get_active_sessions(&self, _user_id: Uuid) -> Vec<&DeviceSession> {
self.sessions.values().collect()
}
pub async fn broadcast_event(
&self,
_user_id: Uuid,
_event: SyncEvent,
_exclude_device: Option<&str>,
) -> Result<(), SyncError> {
Ok(())
}
pub fn resolve_conflict(
&self,
local: &serde_json::Value,
remote: &serde_json::Value,
local_timestamp: DateTime<Utc>,
remote_timestamp: DateTime<Utc>,
strategy: ConflictResolution,
) -> serde_json::Value {
match strategy {
ConflictResolution::UseLocal => local.clone(),
ConflictResolution::UseRemote => remote.clone(),
ConflictResolution::UseNewest => {
if local_timestamp > remote_timestamp {
local.clone()
} else {
remote.clone()
}
}
ConflictResolution::Merge => {
if let (Some(local_obj), Some(remote_obj)) = (local.as_object(), remote.as_object())
{
let mut merged = local_obj.clone();
for (key, value) in remote_obj {
merged.insert(key.clone(), value.clone());
}
serde_json::Value::Object(merged)
} else {
if local_timestamp > remote_timestamp {
local.clone()
} else {
remote.clone()
}
}
}
ConflictResolution::Manual => {
local.clone()
}
}
}
}
impl Default for SyncService {
fn default() -> Self {
Self::new(SyncConfig::default())
}
}
#[derive(Debug, thiserror::Error)]
pub enum SyncError {
#[error("Session not found")]
SessionNotFound,
#[error("Connection failed: {0}")]
ConnectionFailed(String),
#[error("Sync conflict: {0}")]
Conflict(String),
#[error("Offline queue full")]
QueueFull,
#[error("Broadcast failed: {0}")]
BroadcastError(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum WsMessage {
Subscribe {
device_id: String,
token: String,
},
Subscribed {
session_id: String,
},
PushChanges {
version: u64,
changes: Vec<SettingsChange>,
},
ChangesAccepted {
new_version: u64,
},
Event(SyncEvent),
Ping,
Pong,
Error {
code: String,
message: String,
},
}