diff --git a/.idea/dataSources.local.xml b/.idea/dataSources.local.xml index f3f4054..9744bab 100644 --- a/.idea/dataSources.local.xml +++ b/.idea/dataSources.local.xml @@ -1,6 +1,6 @@ - + " diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 155cd37..a2102a8 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -16,15 +16,19 @@ - + + + - - - - - - - + + + + + + + + + diff --git a/src/network/http_routes/channel.rs b/src/network/http_routes/channel.rs index e69de29..5ce3d7b 100644 --- a/src/network/http_routes/channel.rs +++ b/src/network/http_routes/channel.rs @@ -0,0 +1,8 @@ +use axum::Router; +use axum::routing::post; +use crate::network::http::HttpState; + +pub fn create_router() -> Router { + Router::new() + // .route("/auth/", post(join_master_server)) +} \ No newline at end of file diff --git a/src/network/http_routes/message.rs b/src/network/http_routes/message.rs index e69de29..f7cdffd 100644 --- a/src/network/http_routes/message.rs +++ b/src/network/http_routes/message.rs @@ -0,0 +1,8 @@ +use axum::Router; +use axum::routing::post; +use crate::network::http::HttpState; + +pub fn create_router() -> Router { + Router::new() + // .route("/auth/", post(join_master_server)) +} \ No newline at end of file diff --git a/src/network/http_routes/sub_server.rs b/src/network/http_routes/sub_server.rs index e69de29..57b383a 100644 --- a/src/network/http_routes/sub_server.rs +++ b/src/network/http_routes/sub_server.rs @@ -0,0 +1,9 @@ +use axum::Router; +use axum::routing::post; +use crate::network::http::HttpState; + +pub fn create_router() -> Router { + Router::new() + // .route("/auth/", post(join_master_server)) +} + diff --git a/src/network/http_routes/user.rs b/src/network/http_routes/user.rs index e69de29..f7cdffd 100644 --- a/src/network/http_routes/user.rs +++ b/src/network/http_routes/user.rs @@ -0,0 +1,8 @@ +use axum::Router; +use axum::routing::post; +use crate::network::http::HttpState; + +pub fn create_router() -> Router { + Router::new() + // .route("/auth/", post(join_master_server)) +} \ No newline at end of file diff --git a/src/network/http_routes/websocket.rs b/src/network/http_routes/websocket.rs index e69de29..f7cdffd 100644 --- a/src/network/http_routes/websocket.rs +++ b/src/network/http_routes/websocket.rs @@ -0,0 +1,8 @@ +use axum::Router; +use axum::routing::post; +use crate::network::http::HttpState; + +pub fn create_router() -> Router { + Router::new() + // .route("/auth/", post(join_master_server)) +} \ No newline at end of file diff --git a/src/store/mod.rs b/src/store/mod.rs index f09bdd3..af1cb4a 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -1,3 +1,4 @@ pub mod models; pub mod repositories; -pub mod store_service; \ No newline at end of file +pub mod store_service; +mod session; \ No newline at end of file diff --git a/src/store/models/channel.rs b/src/store/models/channel.rs index 2504093..9c7ea79 100644 --- a/src/store/models/channel.rs +++ b/src/store/models/channel.rs @@ -1,3 +1,4 @@ +use std::hash::{Hash, Hasher}; // use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; // use uuid::Uuid; @@ -29,4 +30,18 @@ impl Channel { created_at: Utc::now(), } } +} + +impl PartialEq for Channel { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for Channel {} + +impl Hash for Channel { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } } \ No newline at end of file diff --git a/src/store/models/mod.rs b/src/store/models/mod.rs index 3acd86e..96bac26 100644 --- a/src/store/models/mod.rs +++ b/src/store/models/mod.rs @@ -3,6 +3,7 @@ pub mod channel; pub mod user; pub mod message; pub mod link_sub_server_user; +mod models; pub use sub_server::*; pub use channel::*; diff --git a/src/store/models/sub_server.rs b/src/store/models/sub_server.rs index c1ced2c..7c07336 100644 --- a/src/store/models/sub_server.rs +++ b/src/store/models/sub_server.rs @@ -1,8 +1,10 @@ // L'application peut avoir plusieurs sous servers +use std::hash::{Hash, Hasher}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; +use crate::store::models::Channel; #[derive(Debug, Clone, sqlx::FromRow, Serialize, Deserialize)] pub struct SubServer { @@ -21,4 +23,18 @@ impl SubServer { created_at: Utc::now(), } } +} + +impl PartialEq for SubServer { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for SubServer {} + +impl Hash for SubServer { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } } \ No newline at end of file diff --git a/src/store/session/client.rs b/src/store/session/client.rs new file mode 100644 index 0000000..0529f8e --- /dev/null +++ b/src/store/session/client.rs @@ -0,0 +1,234 @@ + +use std::collections::{HashMap, HashSet}; +use std::hash::{Hash, Hasher}; +use std::net::SocketAddr; +use std::sync::{Arc, Weak}; +use std::time::Instant; +use axum::extract::ws::WebSocket; +use uuid::Uuid; +use crate::store::models::{Channel, SubServer}; +use crate::store::models::user::User; +use crate::utils::shared_store::{SharedArcHashSet, SharedArcMap, SharedArcVec}; + +/// Représente un client connecté au serveur (WebSocket + optionnel UDP) +/// +/// Chaque client a une connexion WebSocket obligatoire et peut avoir une connexion UDP +/// pour la voix s'il rejoint un channel vocal. +#[derive(Debug)] +pub struct Client { + // ===== CHAMPS IMMUTABLES ===== + pub session_id: Uuid, + pub user: User, + pub websocket: WebSocket, + pub created_at: Instant, + + // ===== CHAMPS MUTABLES ===== + pub udp_socket: Option, + pub voice_channel: Option, + + // ===== RÉFÉRENCES INTERNES ===== + /// Référence faible vers le manager pour l'auto-nettoyage + manager: Option>, +} + +impl Client { + /// Crée un nouveau client + pub fn new(user: User, websocket: WebSocket) -> Self { + Self { + session_id: Uuid::new_v4(), + user, + websocket, + created_at: Instant::now(), + udp_socket: None, + voice_channel: None, + manager: None, + } + } + + // ===== MÉTHODES MUTABLES INTERNES ===== + // Ces méthodes sont pub(crate) - elles ne doivent être appelées que via ClientManager::modify_client() + + /// Configure l'adresse UDP (méthode interne) + pub(crate) fn set_udp_socket(&mut self, udp_socket: SocketAddr) { + self.udp_socket = Some(udp_socket); + } + + /// Supprime l'adresse UDP (méthode interne) + pub(crate) fn remove_udp_socket(&mut self) { + self.udp_socket = None; + } + + /// Configure la référence vers le manager (appelé par le manager) + pub(crate) fn set_manager(&mut self, manager: Weak) { + self.manager = Some(manager); + } +} + +impl PartialEq for Client { + fn eq(&self, other: &Self) -> bool { + self.session_id == other.session_id + } +} + +impl Eq for Client {} + +impl Hash for Client { + fn hash(&self, state: &mut H) { + self.session_id.hash(state); + } +} + +/// Nettoyage automatique quand le client est détruit +impl Drop for Client { + fn drop(&mut self) { + if let Some(manager) = self.manager.as_ref().and_then(|w| w.upgrade()) { + manager.cleanup_client_index(&self); + } + } +} + +// ========================================================================= +// CLIENT MANAGER +// ========================================================================= + +/// Gestionnaire centralisé de tous les clients connectés +/// +/// Maintient les clients et leurs index pour des recherches efficaces. +/// Thread-safe via SharedArcMap. +pub struct ClientManager { + /// Map principale des clients par session_id + clients: SharedArcMap, + + // ===== INDEX POUR RECHERCHES RAPIDES ===== + /// Index des clients par channel_id + voice_channel_clients: SharedArcMap>, // channel_id -> HashSet + /// Index des clients par sub_server_id + sub_server_clients: SharedArcMap>, // sub_server_id -> Vec + /// Index des clients par adresse UDP + udp_clients: SharedArcMap, // udp_address -> session_id +} + +impl ClientManager { + /// Crée un nouveau gestionnaire de clients + pub fn new() -> Self { + Self { + clients: SharedArcMap::new(), + voice_channel_clients: SharedArcMap::new(), + sub_server_clients: SharedArcMap::new(), + udp_clients: SharedArcMap::new(), + } + } + + // ===== GESTION DES CLIENTS ===== + + /// Ajoute un nouveau client au gestionnaire + pub fn add_client(self: &Arc, mut client: Client) -> Arc { + let session_id = client.session_id.clone(); + + // Lier le client au manager pour l'auto-nettoyage + client.set_manager(Arc::downgrade(self)); + + // Stocker et retourner + let arc_client = Arc::new(client); + self.clients.insert_arc(session_id, arc_client.clone()); + + arc_client + } + + /// Obtient un client par son session_id + pub fn get_client(&self, session_id: &Uuid) -> Option> { + // Convertir &str en String pour les clés + self.clients.get(session_id) + } + + /// Obtient tous les clients connectés + pub fn get_all_clients(&self) -> Vec> { + self.clients.values().collect() + } + + // ===== OPÉRATIONS DE MODIFICATION ===== + + /// Modifie un client via une closure thread-safe + /// + /// Cette méthode garantit que la modification est atomique et que + /// les index restent cohérents avec l'état du client. + /// + /// # Arguments + /// * `session_id` - L'ID de session du client à modifier + /// * `f` - La closure qui recevra une référence mutable vers le client + /// + /// # Returns + /// `true` si le client a été trouvé et modifié, `false` sinon + pub fn modify_client(&self, session_id: &str, f: F) -> bool + where + F: FnOnce(&mut Client), + { + // Convertir &str en String pour les clés + self.clients.modify(&session_id.to_string(), f) + } + + // ===== OPÉRATIONS ATOMIQUES HAUT NIVEAU ===== + + // Nettoie tous les index d'un client (appelé lors de la suppression) + // pub(crate) fn cleanup_client_indexes(&self, session_id: &str) { + // if let Some(client) = self.get_client(session_id) { + // // Nettoyer l'index des channels + // if let Some(channel_id) = client.current_channel { + // self.remove_from_channel_index(session_id, channel_id); + // } + // + // // Nettoyer l'index des sub-servers + // if let Some(sub_server_id) = client.current_sub_server { + // self.remove_from_sub_server_index(session_id, sub_server_id); + // } + // + // // Nettoyer l'index UDP + // if let Some(udp_address) = client.udp_socket { + // self.udp_clients.remove(&udp_address); + // } + // } + // } +} + +impl ClientManager { + // Delete methods + /// Supprime tous les index d'un client + pub fn cleanup_client_index(&self, client: &Client) { + if let Some(channel) = client.voice_channel.as_ref() { + self.remove_client_from_voice_channel(channel, client); + } + } + + /// Supprime un client du gestionnaire + pub fn remove_client(&self, session_id: &Uuid) { + // Convertir &str en String pour les clés + if let Some(client) = self.clients.get(session_id) { + // Nettoyer tous les index + self.cleanup_client_index(&client); + + // Supprimer le client de la liste principale + self.clients.remove(&session_id); + } + } + + pub fn remove_client_from_voice_channel(&self, voice_channel: &Channel, client: &Client) { + if let Some(channel) = self.voice_channel_clients.get(voice_channel) { + channel.remove(client); + } + } + + pub fn remove_client_from_sub_servers(&self, client: &Client) { + for (sub_server, clients) in self.sub_server_clients.iter() { + let mut to_remove = Vec::::new(); + for (index, client) in clients.iter().enumerate() { + if client == client { + to_remove.push(index); + } + } + + for idx in to_remove.iter() { + clients. + } + } + } +} \ No newline at end of file diff --git a/src/store/session/mod.rs b/src/store/session/mod.rs new file mode 100644 index 0000000..2322d1e --- /dev/null +++ b/src/store/session/mod.rs @@ -0,0 +1 @@ +mod client; \ No newline at end of file diff --git a/src/utils/shared_store.rs b/src/utils/shared_store.rs index 820057c..fbd065a 100644 --- a/src/utils/shared_store.rs +++ b/src/utils/shared_store.rs @@ -244,6 +244,90 @@ impl SharedVec { self.inner.store(Arc::new(new_vec)); } + /// Supprime l'élément à l'index donné et le retourne. + /// Retourne `None` si l'index est hors limites. + pub fn delete(&self, index: usize) -> Option + where + T: Clone, + { + loop { + let current = self.inner.load(); + + if index >= current.len() { + return None; + } + + let mut new_vec = current.as_ref().clone(); + let removed_item = new_vec.remove(index); + + let new_arc = Arc::new(new_vec); + + if self.inner.compare_and_swap(¤t, new_arc).is_ok() { + return Some(removed_item); + } + // Retry si le CAS a échoué + } + } + + /// Supprime et retourne le dernier élément du vecteur. + /// Retourne `None` si le vecteur est vide. + pub fn pop(&self) -> Option + where + T: Clone, + { + loop { + let current = self.inner.load(); + + if current.is_empty() { + return None; + } + + let mut new_vec = current.as_ref().clone(); + let popped_item = new_vec.pop(); + + let new_arc = Arc::new(new_vec); + + if self.inner.compare_and_swap(¤t, new_arc).is_ok() { + return popped_item; + } + // Retry si le CAS a échoué + } + } + + /// Supprime tous les éléments qui correspondent au prédicat donné. + /// Retourne le nombre d'éléments supprimés. + pub fn delete_matching(&self, predicate: F) -> usize + where + T: Clone, + F: Fn(&T) -> bool, + { + loop { + let current = self.inner.load(); + let mut new_vec = Vec::with_capacity(current.len()); + let mut removed_count = 0; + + for item in current.iter() { + if predicate(item) { + removed_count += 1; + } else { + new_vec.push(item.clone()); + } + } + + if removed_count == 0 { + return 0; + } + + let new_arc = Arc::new(new_vec); + + if self.inner.compare_and_swap(¤t, new_arc).is_ok() { + return removed_count; + } + // Retry si le CAS a échoué + } + } + + /// Clear optimisé /// /// Exemple : `clients.clear(); // Vide la liste` @@ -852,6 +936,116 @@ impl SharedArcVec { self.inner.store(Arc::new(Vec::new())); } + /// Supprime l'élément à l'index donné et le retourne. + /// Retourne `None` si l'index est hors limites. + pub fn delete(&self, index: usize) -> Option> { + loop { + let current = self.inner.load(); + + if index >= current.len() { + return None; + } + + let mut new_vec = current.as_ref().clone(); + let removed_item = new_vec.remove(index); + + let new_arc = Arc::new(new_vec); + + if self.inner.compare_and_swap(¤t, new_arc).is_ok() { + return Some(removed_item); + } + // Retry si le CAS a échoué + } + } + + /// Supprime et retourne le dernier élément du vecteur. + /// Retourne `None` si le vecteur est vide. + pub fn pop(&self) -> Option> { + loop { + let current = self.inner.load(); + + if current.is_empty() { + return None; + } + + let mut new_vec = current.as_ref().clone(); + let popped_item = new_vec.pop(); + + let new_arc = Arc::new(new_vec); + + if self.inner.compare_and_swap(¤t, new_arc).is_ok() { + return popped_item; + } + // Retry si le CAS a échoué + } + } + + /// Supprime tous les éléments qui correspondent au prédicat donné. + /// Retourne le nombre d'éléments supprimés. + pub fn delete_matching(&self, predicate: F) -> usize + where + F: Fn(&T) -> bool, + { + loop { + let current = self.inner.load(); + let mut new_vec = Vec::with_capacity(current.len()); + let mut removed_count = 0; + + for item in current.iter() { + if predicate(item.as_ref()) { + removed_count += 1; + } else { + new_vec.push(item.clone()); + } + } + + if removed_count == 0 { + return 0; + } + + let new_arc = Arc::new(new_vec); + + if self.inner.compare_and_swap(¤t, new_arc).is_ok() { + return removed_count; + } + // Retry si le CAS a échoué + } + } + + /// Supprime le premier élément trouvé qui correspond au prédicat. + /// Retourne `Some(Arc)` si un élément a été trouvé et supprimé, `None` sinon. + pub fn delete_first_matching(&self, predicate: F) -> Option> + where + F: Fn(&T) -> bool, + { + loop { + let current = self.inner.load(); + let mut found_index = None; + + for (index, item) in current.iter().enumerate() { + if predicate(item.as_ref()) { + found_index = Some(index); + break; + } + } + + if let Some(index) = found_index { + let mut new_vec = current.as_ref().clone(); + let removed_item = new_vec.remove(index); + + let new_arc = Arc::new(new_vec); + + if self.inner.compare_and_swap(¤t, new_arc).is_ok() { + return Some(removed_item); + } + // Retry si le CAS a échoué + } else { + return None; + } + } + } + + /// Iteration retournant Arc pub fn iter(&self) -> impl Iterator> + '_ { let vec_ref = self.inner.load_full();