Setting up the disconnect logic

Ok, the disconnect part is in the same lambda but it has a few more strings attached to it. if you remember the first chapter:

All the players are disconnected from the game when a winner is decided. 
Which cleans up all the connection information and game relevant information. 

Why clean up all the connection information? Isn't it easier to reuse the connection and play the next game if people are interested. The maximum connection time for apigatewy is 2 hours so it's better to reset the timer not to run into problems later.

On disconnect

#![allow(unused)]
fn main() {
async fn on_disconnect(event: Event) {
    let player_s = get_player(
        event.request_context.connection_id.to_string(),
        PlayerState::Started,
    )
    .await;
    clean_up_game(player_s, &event.request_context.domain_name).await;

    remove_connection(event.request_context.connection_id).await;
}

async fn clean_up_game(player: Result<Player, String>, domain_name: &String) {
    match player {
        Ok(player) => {
            let client = GameRepoDynamo::new();
            let players_to_inform = client
                .delete_game_returns_contained_connections(player.game_id.to_string())
                .await;
            let websocket_client = WebsocketClient::new(&domain_name);
            match players_to_inform {
                Ok(players) => {
                    for to_inform_player in players.into_iter() {
                        if to_inform_player == player.connection_id {
                            continue;
                        }
                        websocket_client
                            .send_action(
                                &to_inform_player,
                                RockAction::send_decided(
                                    GameOutcome::PlayerLeft,
                                    Plays::Paper,
                                    None,
                                ),
                            )
                            .await
                        
                        player_repo::remove_connection(to_inform_player.to_string()).await;

                        

                    }
                }
                Err(e) => println!("no players to inform {}", e),
            }
        }
        Err(e) => println!("no game to clean up {}", e),
    }
}

pub async fn remove_connection(connection_id: String) {
    // instead of scanning for the key that exists the started player and the player that is waiting in the queue is removed.
    let player = PlayerKey::new(connection_id.clone(), None);
    let started_player = PlayerKey::new(connection_id, Some("started".to_string()));
    let started_item = serde_dynamodb::to_hashmap(&started_player).unwrap();
    let waiting_item = serde_dynamodb::to_hashmap(&player).unwrap();
    
    remove_from_dynamodb(started_item).await;
    remove_from_dynamodb(waiting_item).await;
}

pub async fn remove_from_dynamodb(key: HashMap<String, AttributeValue>) {
    let _result = CLIENT
        .delete_item(DeleteItemInput {
            key: key,
            table_name: TABLE_NAME.into(),
            ..Default::default()
        })
        .await;
}
}

Ok so the logic is quite clear. Someone leaves the game and the other is informed of them leaving the game and there connection is closed. I remove both possible existences of the connection instead of doing a scan. Something That might be improved as well. The client is a dynamodb client from the rusoto package. You can interact with a dynamodb in this way quite easily.

Interacting with the websocket

Another piece of magic is websocket_client.send_action on the inside is again a rusoto package which interacts with the AWS api gateway. It ask to send a certain action to the client. So you provide the client id and the message and the api gateway will try to push it to the client.

Using generics with a limitation we can easily reuse this for other messages we want to send to clients.

#![allow(unused)]
fn main() {
use rusoto_apigatewaymanagementapi::{ApiGatewayManagementApi, ApiGatewayManagementApiClient, DeleteConnectionRequest, PostToConnectionError, PostToConnectionRequest};
use rusoto_core::{Region, RusotoError};
use serde::Serialize;
pub struct WebsocketClient {
    client: ApiGatewayManagementApiClient,
}
impl WebsocketClient {
    pub fn new(domain_name: &String) -> WebsocketClient {
        WebsocketClient {
            client: get_api_client(&domain_name),
        }
    }
    pub async fn send_action<T>(&self, connection_id: &String, message: T) -> Result<(), RusotoError<PostToConnectionError>>
    where
        T: Serialize,
    {
        let data = serde_json::to_string(&message).unwrap();
        let result = self
            .client
            .post_to_connection(PostToConnectionRequest {
                connection_id: connection_id.to_string(),
                data: data.into(),
            })
            .await;
            match result {
                Ok(_) => Ok(()),
                Err(e) => {
                    println!("receved error on sending action for connection {} error {}",connection_id,e);
                    
                    Err(e)
                },
            }
        
    }
    pub async fn disconnect_client(&self, connection_id: &String)-> Result<(),String>{
        let result =  self.client.delete_connection(DeleteConnectionRequest{ connection_id: connection_id.to_string() }).await;
        match result {
            Ok(_) => Ok(()),
            Err(e) => {
                println!("error in disconnecting client {}",e);
                Err(format!("{}",e))},
        }
    }
}

fn get_api_client(domain_name: &String) -> ApiGatewayManagementApiClient {
    let endpoint = format!("https://{}", domain_name);

    rusoto_apigatewaymanagementapi::ApiGatewayManagementApiClient::new(Region::Custom {
        name: "eu-west-1".to_owned(),
        endpoint,
    })
}

}

Thanks for reading

I enjoyed trying to make everything and I hope you'll enjoy reading it. If you are excited to have a chat or think I could improve feel free to write me on LinkedIn. I also have portfolio website where I put most of what I make and host trial projects portfolio.rohanengosia.com.