From bd19ae2c1720ded24f3975c399908685446bd331 Mon Sep 17 00:00:00 2001 From: nafsonig Date: Thu, 27 Aug 2026 14:07:32 +0100 Subject: [PATCH 01/11] implemented the WebSocket Reconnection State Machine with Grace Period Heartbeat --- backend/modules/api/src/ws.rs | 85 +++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/backend/modules/api/src/ws.rs b/backend/modules/api/src/ws.rs index a5affb6..a3a056b 100644 --- a/backend/modules/api/src/ws.rs +++ b/backend/modules/api/src/ws.rs @@ -12,12 +12,91 @@ use tracing::{error, info, warn}; use uuid::Uuid; use sea_orm::{DatabaseConnection, EntityTrait}; use db_entity::game; -use sea_orm::{DatabaseConnection, EntityTrait}; -use db_entity::game; +use db::DbPool; +use dto::games::GameStatus; // For Redis Pub/Sub // Redis pub/sub integration removed for test stability in CI environment use tokio::task::JoinHandle; +use chrono::{DateTime, Utc}; + +/// Player connection status +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ConnectionStatus { + Connected, + Reconnecting, + Disconnected, +} + +/// Player state within a game session +#[derive(Debug, Clone)] +pub struct PlayerConnectionState { + pub player_id: Uuid, + pub status: ConnectionStatus, + pub disconnected_at: Option>, + pub grace_timer: Option>, + pub addr: Option>, +} + +/// Game session state tracking all players in a game +#[derive(Debug, Clone)] +pub struct GameSessionState { + pub game_id: String, + pub players: HashMap, + pub is_active: bool, +} + +/// Connection state tracker actor that manages all active game sessions +pub struct ConnectionStateTracker { + game_sessions: HashMap, + db_pool: Option, +} + +/// Message to mark a player as disconnected (start grace period) +#[derive(Message)] +#[rtype(result = "()")] +pub struct PlayerDisconnected { + pub game_id: String, + pub player_id: Uuid, +} + +/// Message to mark a player as reconnected +#[derive(Message)] +#[rtype(result = "()")] +pub struct PlayerReconnected { + pub game_id: String, + pub player_id: Uuid, + pub addr: Recipient, +} + +/// Message sent when grace period expires +#[derive(Message)] +#[rtype(result = "()")] +pub struct GracePeriodExpired { + pub game_id: String, + pub player_id: Uuid, +} + +/// Message to get full game state for syncing on reconnect +#[derive(Message)] +#[rtype(result = "Result")] +pub struct GetGameState { + pub game_id: String, +} + +/// OpponentDisconnected message sent to connected opponent with grace seconds left +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct OpponentDisconnectedPayload { + pub grace_seconds_left: u32, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(tag = "type", content = "payload")] +pub enum ExtendedWsMessage { + Original(WsMessage), + OpponentDisconnected(OpponentDisconnectedPayload), + OpponentReconnected, +} /// Core WebSocket message types #[derive(Message, Serialize, Deserialize, Clone, Debug, PartialEq)] @@ -431,4 +510,4 @@ mod tests { assert_eq!(received1, msg); assert_eq!(received2, msg); } -} +} \ No newline at end of file From 2d92b67b6cb871c603e980d236419adff3a91ba6 Mon Sep 17 00:00:00 2001 From: nafsonig Date: Thu, 27 Aug 2026 14:07:37 +0100 Subject: [PATCH 02/11] implemented the WebSocket Reconnection State Machine with Grace Period Heartbeat --- backend/modules/api/src/ws.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/modules/api/src/ws.rs b/backend/modules/api/src/ws.rs index a3a056b..595fa46 100644 --- a/backend/modules/api/src/ws.rs +++ b/backend/modules/api/src/ws.rs @@ -13,7 +13,8 @@ use uuid::Uuid; use sea_orm::{DatabaseConnection, EntityTrait}; use db_entity::game; use db::DbPool; -use dto::games::GameStatus; +use dto::games::{GameStatus, GameDisplayDTO}; +use error::error::ApiError; // For Redis Pub/Sub // Redis pub/sub integration removed for test stability in CI environment From 16aa788a728f50ae879dd2ad49e810e4dc37fe6b Mon Sep 17 00:00:00 2001 From: nafsonig Date: Thu, 27 Aug 2026 14:07:55 +0100 Subject: [PATCH 03/11] implemented the WebSocket Reconnection State Machine with Grace Period Heartbeat --- backend/modules/api/src/ws.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/backend/modules/api/src/ws.rs b/backend/modules/api/src/ws.rs index 595fa46..946d7fe 100644 --- a/backend/modules/api/src/ws.rs +++ b/backend/modules/api/src/ws.rs @@ -99,6 +99,12 @@ pub enum ExtendedWsMessage { OpponentReconnected, } +/// OpponentDisconnected message payload +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct OpponentDisconnectedPayload { + pub grace_seconds_left: u32, +} + /// Core WebSocket message types #[derive(Message, Serialize, Deserialize, Clone, Debug, PartialEq)] #[rtype(result = "()")] @@ -126,6 +132,14 @@ pub enum WsMessage { token: String, expires_in: u32, }, + OpponentDisconnected(OpponentDisconnectedPayload), + OpponentReconnected, + FullStateSync { + fen: String, + move_list: Vec, + white_time: u32, + black_time: u32, + }, } /// Actor messages From e3991f86872bd2adfdeacf17f7a948dc28830ecd Mon Sep 17 00:00:00 2001 From: nafsonig Date: Thu, 27 Aug 2026 14:08:13 +0100 Subject: [PATCH 04/11] implemented the WebSocket Reconnection State Machine with Grace Period Heartbeat --- backend/modules/api/src/ws.rs | 238 ++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) diff --git a/backend/modules/api/src/ws.rs b/backend/modules/api/src/ws.rs index 946d7fe..b817f17 100644 --- a/backend/modules/api/src/ws.rs +++ b/backend/modules/api/src/ws.rs @@ -222,6 +222,244 @@ impl Handler for LobbyState { } } +impl Default for ConnectionStateTracker { + fn default() -> Self { + Self::new() + } +} + +impl ConnectionStateTracker { + const GRACE_PERIOD_SECONDS: u64 = 60; + + pub fn new(db_pool: Option) -> Self { + ConnectionStateTracker { + game_sessions: HashMap::new(), + db_pool, + } + } + + /// Get or create a game session + fn get_or_create_session(&mut self, game_id: String) -> &mut GameSessionState { + self.game_sessions.entry(game_id.clone()).or_insert_with(|| { + GameSessionState { + game_id, + players: HashMap::new(), + is_active: true, + } + }) + } + + /// Broadcast message to all other players in the game + fn broadcast_to_other_players( + &mut self, + session: &GameSessionState, + exclude_player_id: Uuid, + message: WsMessage, + ) { + for (player_id, player_state) in &session.players { + if *player_id != exclude_player_id { + if let Some(addr) = &player_state.addr { + let _ = addr.do_send(message.clone()); + } + } + } + } +} + +impl Actor for ConnectionStateTracker { + type Context = Context; +} + +/// Handle PlayerDisconnected message - start grace period timer +impl Handler for ConnectionStateTracker { + type Result = (); + + fn handle(&mut self, msg: PlayerDisconnected, ctx: &mut Context) { + let session = self.get_or_create_session(msg.game_id.clone()); + + // Only process if game is active and player exists + if !session.is_active { + return; + } + + if let Some(player_state) = session.players.get_mut(&msg.player_id) { + // Only start timer if not already disconnected + if player_state.status != ConnectionStatus::Disconnected { + player_state.status = ConnectionStatus::Reconnecting; + player_state.disconnected_at = Some(Utc::now()); + player_state.addr = None; // Clear old address + + info!( + "Player {} disconnected from game {}, starting {}s grace period", + msg.player_id, msg.game_id, Self::GRACE_PERIOD_SECONDS + ); + + // Notify opponent that player disconnected with grace period + self.broadcast_to_other_players( + session, + msg.player_id, + WsMessage::OpponentDisconnected(OpponentDisconnectedPayload { + grace_seconds_left: Self::GRACE_PERIOD_SECONDS as u32, + }), + ); + + // Spawn grace period timer + let tracker_addr = ctx.address().clone(); + let game_id_clone = msg.game_id.clone(); + let player_id_clone = msg.player_id; + + let timer_handle = tokio::spawn(async move { + tokio::time::sleep(std::time::Duration::from_secs(Self::GRACE_PERIOD_SECONDS)).await; + tracker_addr.do_send(GracePeriodExpired { + game_id: game_id_clone, + player_id: player_id_clone, + }); + }); + + player_state.grace_timer = Some(timer_handle); + } + } + } +} + +/// Handle PlayerReconnected message - cancel timer, sync state +impl Handler for ConnectionStateTracker { + type Result = (); + + fn handle(&mut self, msg: PlayerReconnected, ctx: &mut Context) { + let session = match self.game_sessions.get_mut(&msg.game_id) { + Some(s) => s, + None => return, + }; + + if !session.is_active { + return; + } + + if let Some(player_state) = session.players.get_mut(&msg.player_id) { + // Cancel any existing grace timer + if let Some(timer) = player_state.grace_timer.take() { + timer.abort(); + info!( + "Player {} reconnected to game {}, grace period cancelled", + msg.player_id, msg.game_id + ); + } + + // Update player state + player_state.status = ConnectionStatus::Connected; + player_state.disconnected_at = None; + player_state.addr = Some(msg.addr.clone()); + + // Notify opponent that player reconnected + self.broadcast_to_other_players( + session, + msg.player_id, + WsMessage::OpponentReconnected, + ); + + // If we have a DB pool, fetch full game state to sync + if let Some(db_pool) = &self.db_pool { + let db_pool_clone = db_pool.clone(); + let addr_clone = msg.addr.clone(); + let game_id_uuid = match Uuid::parse_str(&msg.game_id) { + Ok(id) => id, + Err(_) => return, + }; + + // Spawn task to fetch game state and send full sync + tokio::spawn(async move { + match crate::service::games::GameService::get_game(&db_pool_clone, game_id_uuid).await { + Ok(game_state) => { + // Convert move history to Vec + let move_list: Vec = game_state.move_history + .into_iter() + .map(|m| m.to_string()) + .collect(); + + let sync_message = WsMessage::FullStateSync { + fen: game_state.current_fen, + move_list, + white_time: game_state.white_time_remaining as u32, + black_time: game_state.black_time_remaining as u32, + }; + + let _ = addr_clone.do_send(sync_message); + info!("Sent full state sync to reconnected player {} in game {}", msg.player_id, msg.game_id); + } + Err(e) => { + error!("Failed to fetch game state for sync: {}", e); + } + } + }); + } + } else { + // New player joining the game + session.players.insert(msg.player_id, PlayerConnectionState { + player_id: msg.player_id, + status: ConnectionStatus::Connected, + disconnected_at: None, + grace_timer: None, + addr: Some(msg.addr), + }); + info!("New player {} added to game {}", msg.player_id, msg.game_id); + } + } +} + +/// Handle GracePeriodExpired message - trigger abandonment timeout +impl Handler for ConnectionStateTracker { + type Result = (); + + fn handle(&mut self, msg: GracePeriodExpired, _: &mut Context) { + let session = match self.game_sessions.get_mut(&msg.game_id) { + Some(s) => s, + None => return, + }; + + if !session.is_active { + return; + } + + if let Some(player_state) = session.players.get_mut(&msg.player_id) { + if player_state.status == ConnectionStatus::Reconnecting { + info!( + "Grace period expired for player {} in game {}, triggering abandonment", + msg.player_id, msg.game_id + ); + + // Mark player as disconnected permanently + player_state.status = ConnectionStatus::Disconnected; + player_state.grace_timer = None; + + // If we have a DB pool, call abandon_game to declare timeout + if let Some(db_pool) = &self.db_pool { + let db_pool_clone = db_pool.clone(); + let game_id_uuid = match Uuid::parse_str(&msg.game_id) { + Ok(id) => id, + Err(_) => return, + }; + let player_id_clone = msg.player_id; + + tokio::spawn(async move { + match crate::service::games::GameService::abandon_game(&db_pool_clone, game_id_uuid, player_id_clone).await { + Ok(_) => { + info!("Successfully marked game {} as abandoned by player {}", game_id_uuid, player_id_clone); + } + Err(e) => { + error!("Failed to mark game as abandoned: {}", e); + } + } + }); + + // Mark game as inactive to prevent further processing + session.is_active = false; + } + } + } + } +} + /// WebSocket session actor pub struct WsSession { pub game_id: String, From 74ba1633e47ba292424acdc69c2799b836aabd65 Mon Sep 17 00:00:00 2001 From: nafsonig Date: Thu, 27 Aug 2026 14:08:26 +0100 Subject: [PATCH 05/11] implemented the WebSocket Reconnection State Machine with Grace Period Heartbeat --- backend/modules/api/src/ws.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/modules/api/src/ws.rs b/backend/modules/api/src/ws.rs index b817f17..4bc768f 100644 --- a/backend/modules/api/src/ws.rs +++ b/backend/modules/api/src/ws.rs @@ -464,6 +464,7 @@ impl Handler for ConnectionStateTracker { pub struct WsSession { pub game_id: String, pub lobby: Addr, + pub connection_tracker: Addr, pub hb: std::time::Instant, pub user_id: i32, pub player_id: Uuid, From 2269d781e11e78a23068d3fc513cb70163628c91 Mon Sep 17 00:00:00 2001 From: nafsonig Date: Thu, 27 Aug 2026 14:08:31 +0100 Subject: [PATCH 06/11] implemented the WebSocket Reconnection State Machine with Grace Period Heartbeat --- backend/modules/api/src/ws.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend/modules/api/src/ws.rs b/backend/modules/api/src/ws.rs index 4bc768f..ca50d49 100644 --- a/backend/modules/api/src/ws.rs +++ b/backend/modules/api/src/ws.rs @@ -517,6 +517,13 @@ impl Actor for WsSession { let addr = ctx.address().recipient(); self.lobby.do_send(Connect { game_id: self.game_id.clone(), + addr: addr.clone(), + }); + + // Notify connection tracker that player reconnected/connected + self.connection_tracker.do_send(PlayerReconnected { + game_id: self.game_id.clone(), + player_id: self.player_id, addr, }); From 602431a06f8dd18e2b33a824435759808984e8e2 Mon Sep 17 00:00:00 2001 From: nafsonig Date: Thu, 27 Aug 2026 14:08:56 +0100 Subject: [PATCH 07/11] implemented the WebSocket Reconnection State Machine with Grace Period Heartbeat --- backend/modules/api/src/ws.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/backend/modules/api/src/ws.rs b/backend/modules/api/src/ws.rs index ca50d49..3338b83 100644 --- a/backend/modules/api/src/ws.rs +++ b/backend/modules/api/src/ws.rs @@ -538,11 +538,13 @@ impl Actor for WsSession { if let Ok(reconnect_token) = self.generate_reconnect_token() { let reconnect_msg = WsMessage::ReconnectToken { token: reconnect_token, - expires_in: 30, + expires_in: 60, // Match grace period }; // Try to send the reconnection token - ctx.address().do_send(reconnect_msg); + if let Err(e) = ctx.address().try_send(reconnect_msg) { + warn!("Could not send reconnection token (connection already closed): {}", e); + } info!("Sent reconnection token for user: {}", self.username); } else { error!( @@ -556,6 +558,13 @@ impl Actor for WsSession { game_id: self.game_id.clone(), addr, }); + + // Notify connection tracker that player disconnected - start grace period + self.connection_tracker.do_send(PlayerDisconnected { + game_id: self.game_id.clone(), + player_id: self.player_id, + }); + // Cancel Redis subscription task if running if let Some(handle) = self.redis_sub_task.take() { handle.abort(); @@ -611,6 +620,7 @@ pub async fn ws_route( req: HttpRequest, stream: web::Payload, lobby: web::Data>, + connection_tracker: web::Data>, ) -> Result { let auth_header = req .headers() @@ -654,6 +664,7 @@ pub async fn ws_route( WsSession { game_id, lobby: lobby.get_ref().clone(), + connection_tracker: connection_tracker.get_ref().clone(), hb: std::time::Instant::now(), user_id: claims.user_id, player_id: claims.player_id, From 83dab154106776b7601eff385457885e96415a98 Mon Sep 17 00:00:00 2001 From: nafsonig Date: Thu, 27 Aug 2026 14:09:28 +0100 Subject: [PATCH 08/11] implemented the WebSocket Reconnection State Machine with Grace Period Heartbeat --- backend/modules/api/src/ws.rs | 137 ++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/backend/modules/api/src/ws.rs b/backend/modules/api/src/ws.rs index 3338b83..b8e8438 100644 --- a/backend/modules/api/src/ws.rs +++ b/backend/modules/api/src/ws.rs @@ -748,6 +748,143 @@ mod tests { async fn test_broadcast_to_two_clients() { let lobby = LobbyState::new().start(); let (tx1, mut rx1) = unbounded_channel(); + + #[actix_web::test] + async fn test_websocket_drop_and_reconnect() { + // Create connection tracker with no DB pool for testing + let connection_tracker = ConnectionStateTracker::new(None).start(); + + // Create two test players + let player1_id = Uuid::new_v4(); + let player2_id = Uuid::new_v4(); + let game_id = Uuid::new_v4().to_string(); + + // Channel to receive messages for player 2 (opponent) + let (tx2, mut rx2) = unbounded_channel(); + let test_recipient = TestRecipient { tx: tx2 }.start(); + let player2_addr = test_recipient.recipient(); + + // First, player 2 connects + connection_tracker.do_send(PlayerReconnected { + game_id: game_id.clone(), + player_id: player2_id, + addr: player2_addr, + }); + + // Player 1 connects + let (tx1, mut rx1) = unbounded_channel(); + let test_recipient1 = TestRecipient { tx: tx1 }.start(); + let player1_addr = test_recipient1.recipient(); + + connection_tracker.do_send(PlayerReconnected { + game_id: game_id.clone(), + player_id: player1_id, + addr: player1_addr, + }); + + // Verify both players are connected + let session = connection_tracker.state().game_sessions.get(&game_id).unwrap(); + assert_eq!(session.players.get(&player1_id).unwrap().status, ConnectionStatus::Connected); + assert_eq!(session.players.get(&player2_id).unwrap().status, ConnectionStatus::Connected); + + // Player 1 disconnects - this should start the grace period + connection_tracker.do_send(PlayerDisconnected { + game_id: game_id.clone(), + player_id: player1_id, + }); + + // Player 2 should receive OpponentDisconnected message with 60s grace + let msg = tokio::time::timeout(std::time::Duration::from_millis(100), rx2.recv()).await; + assert!(msg.is_ok()); + if let Ok(Some(WsMessage::OpponentDisconnected(payload))) = msg { + assert_eq!(payload.grace_seconds_left, 60); + } else { + panic!("Expected OpponentDisconnected message"); + } + + // Verify player 1 is in Reconnecting state + let session = connection_tracker.state().game_sessions.get(&game_id).unwrap(); + assert_eq!(session.players.get(&player1_id).unwrap().status, ConnectionStatus::Reconnecting); + assert!(session.players.get(&player1_id).unwrap().grace_timer.is_some()); + + // Wait 10 seconds (simulate brief network drop) + tokio::time::sleep(std::time::Duration::from_secs(10)).await; + + // Player 1 reconnects with new connection + let (tx1_new, mut rx1_new) = unbounded_channel(); + let test_recipient1_new = TestRecipient { tx: tx1_new }.start(); + let player1_new_addr = test_recipient1_new.recipient(); + + connection_tracker.do_send(PlayerReconnected { + game_id: game_id.clone(), + player_id: player1_id, + addr: player1_new_addr, + }); + + // Player 2 should receive OpponentReconnected message + let msg = tokio::time::timeout(std::time::Duration::from_millis(100), rx2.recv()).await; + assert!(msg.is_ok()); + if let Ok(Some(WsMessage::OpponentReconnected)) = msg { + // Success - opponent was notified of reconnection + } else { + panic!("Expected OpponentReconnected message"); + } + + // Verify player 1 is back to Connected state, timer was cancelled + let session = connection_tracker.state().game_sessions.get(&game_id).unwrap(); + assert_eq!(session.players.get(&player1_id).unwrap().status, ConnectionStatus::Connected); + assert!(session.players.get(&player1_id).unwrap().grace_timer.is_none()); + } + + #[actix_web::test] + async fn test_grace_period_expiry() { + // Create connection tracker with no DB pool for testing + let connection_tracker = ConnectionStateTracker::new(None).start(); + + let player1_id = Uuid::new_v4(); + let player2_id = Uuid::new_v4(); + let game_id = Uuid::new_v4().to_string(); + + // Player 2 connects + let (tx2, mut rx2) = unbounded_channel(); + let test_recipient = TestRecipient { tx: tx2 }.start(); + connection_tracker.do_send(PlayerReconnected { + game_id: game_id.clone(), + player_id: player2_id, + addr: test_recipient.recipient(), + }); + + // Player 1 connects + let (tx1, _rx1) = unbounded_channel(); + let test_recipient1 = TestRecipient { tx: tx1 }.start(); + connection_tracker.do_send(PlayerReconnected { + game_id: game_id.clone(), + player_id: player1_id, + addr: test_recipient1.recipient(), + }); + + // Player 1 disconnects + connection_tracker.do_send(PlayerDisconnected { + game_id: game_id.clone(), + player_id: player1_id, + }); + + // Verify player 1 is reconnecting + let session = connection_tracker.state().game_sessions.get(&game_id).unwrap(); + assert_eq!(session.players.get(&player1_id).unwrap().status, ConnectionStatus::Reconnecting); + + // Wait for grace period to expire (we set it to 60s normally, but in test we can check the logic) + // For this test, we manually send the expiry message to simulate timer expiration + connection_tracker.do_send(GracePeriodExpired { + game_id: game_id.clone(), + player_id: player1_id, + }); + + // Verify player 1 is now permanently disconnected + let session = connection_tracker.state().game_sessions.get(&game_id).unwrap(); + assert_eq!(session.players.get(&player1_id).unwrap().status, ConnectionStatus::Disconnected); + assert!(!session.is_active); + } let (tx2, mut rx2) = unbounded_channel(); let recipient1 = TestRecipient { tx: tx1 }.start().recipient(); let recipient2 = TestRecipient { tx: tx2 }.start().recipient(); From 06aee72284037a44b521bd80d87b3399e4e2bd0d Mon Sep 17 00:00:00 2001 From: nafsonig Date: Thu, 27 Aug 2026 14:09:36 +0100 Subject: [PATCH 09/11] implemented the WebSocket Reconnection State Machine with Grace Period Heartbeat --- backend/modules/api/src/server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/modules/api/src/server.rs b/backend/modules/api/src/server.rs index 1e10157..f8e0374 100644 --- a/backend/modules/api/src/server.rs +++ b/backend/modules/api/src/server.rs @@ -14,7 +14,7 @@ use crate::idempotency::IdempotencyMiddleware; use crate::players::{add_player, delete_player, find_player_by_id, update_player}; use crate::rate_limiter::RedisRateLimiter; use crate::request_id::RequestIdMiddleware; -use crate::ws::{ws_route, LobbyState}; +use crate::ws::{ws_route, LobbyState, ConnectionStateTracker}; use actix::Actor; use actix_cors::Cors; use actix_governor::{Governor, GovernorConfigBuilder}; @@ -422,4 +422,4 @@ pub async fn main() -> std::io::Result<()> { }); server.await -} +} \ No newline at end of file From e840c262afe6882f092f7d03dede98cb5c82e223 Mon Sep 17 00:00:00 2001 From: nafsonig Date: Thu, 27 Aug 2026 14:09:47 +0100 Subject: [PATCH 10/11] implemented the WebSocket Reconnection State Machine with Grace Period Heartbeat --- backend/modules/api/src/server.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/modules/api/src/server.rs b/backend/modules/api/src/server.rs index f8e0374..d8993b9 100644 --- a/backend/modules/api/src/server.rs +++ b/backend/modules/api/src/server.rs @@ -163,6 +163,9 @@ pub async fn main() -> std::io::Result<()> { // Create a shared LobbyState actor let lobby = LobbyState::new().start(); + + // Create a shared ConnectionStateTracker actor with DB pool for game session management + let connection_tracker = ConnectionStateTracker::new(Some((*db_pool).clone())).start(); // Load AppConfig let config = AppConfig::from_env(); From fb9ea78c366a6124f8829cf60a7018b30b3f1cde Mon Sep 17 00:00:00 2001 From: nafsonig Date: Thu, 27 Aug 2026 14:10:00 +0100 Subject: [PATCH 11/11] implemented the WebSocket Reconnection State Machine with Grace Period Heartbeat --- backend/modules/api/src/server.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/modules/api/src/server.rs b/backend/modules/api/src/server.rs index d8993b9..cfc29e5 100644 --- a/backend/modules/api/src/server.rs +++ b/backend/modules/api/src/server.rs @@ -194,6 +194,7 @@ pub async fn main() -> std::io::Result<()> { let jwt_secret = jwt_secret.clone(); let matchmaking_service = matchmaking_service.clone(); let puzzle_service = puzzle_service.clone(); + let connection_tracker = connection_tracker.clone(); // Configure CORS middleware with environment variables for flexibility let cors = { @@ -261,6 +262,7 @@ pub async fn main() -> std::io::Result<()> { .app_data(web::Data::from(db_pool.clone())) .app_data(web::Data::new(jwt_service.clone())) .app_data(web::Data::new(lobby.clone())) + .app_data(web::Data::new(connection_tracker.clone())) .app_data(web::Data::new(matchmaking_service.clone())) .app_data(web::Data::new(puzzle_service.clone())) .app_data(web::Data::new(metrics_collector.clone()))