Skip to content

Commit

Permalink
User leaves a room functionality + declaritive macros
Browse files Browse the repository at this point in the history
  • Loading branch information
ddimaria committed Dec 4, 2023
1 parent e0a6a40 commit 8660be0
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 52 deletions.
23 changes: 8 additions & 15 deletions quadratic-multiplayer/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

use std::sync::Arc;

use anyhow::{anyhow, Result};
use anyhow::Result;
use axum::extract::ws::{Message, WebSocket};
use futures_util::stream::SplitSink;
use futures_util::SinkExt;
Expand Down Expand Up @@ -107,7 +107,6 @@ pub(crate) async fn handle_message(
let is_new = state.enter_room(file_id, &user).await;
let room = state.get_room(&file_id).await?;
let response = MessageResponse::Room { room };
tracing::info!("user {} entered room", user.id);

// only broadcast if the user is new to the room
if is_new {
Expand All @@ -118,21 +117,15 @@ pub(crate) async fn handle_message(
}

MessageRequest::LeaveRoom { user_id, file_id } => {
if let Ok(room) = state.get_room(&file_id).await {
if state.leave_room(file_id, &user_id).await {
let response = MessageResponse::Room { room };
tracing::info!("user {} left room", user_id);

broadcast(user_id, file_id, Arc::clone(&state), response.clone())?;
let is_not_empty = state.leave_room(file_id, &user_id).await?;
let room = state.get_room(&file_id).await?;
let response = MessageResponse::Room { room };

Ok(response)
} else {
// this is expected when the room has been deleted
Err(anyhow!(""))
}
} else {
Err(anyhow!("Room {} not found", file_id))
if is_not_empty {
broadcast(user_id, file_id, Arc::clone(&state), response.clone())?
}

Check warning on line 126 in quadratic-multiplayer/src/message.rs

View check run for this annotation

Codecov / codecov/patch

quadratic-multiplayer/src/message.rs#L126

Added line #L126 was not covered by tests

Ok(response)
}

// User moves their mouse
Expand Down
8 changes: 4 additions & 4 deletions quadratic-multiplayer/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,22 +188,22 @@ pub(crate) mod tests {
let user = new_user();
let user_id = user.id.clone();
let user2 = new_user();
let user_id2 = user2.id.clone();
let file_id = Uuid::new_v4();
let request = MessageRequest::LeaveRoom {
user_id: user_id.clone(),
user_id: user_id,
file_id,
};
let expected = MessageResponse::Room {
room: Room {
file_id,
users: vec![(user_id, user.clone())].into_iter().collect(),
users: vec![(user2.id.clone(), user2.clone())]
.into_iter()
.collect(),
},
};

state.enter_room(file_id, &user).await;
state.enter_room(file_id, &user2).await;
state.leave_room(file_id, &user_id2).await;

let response = integration_test(state.clone(), request).await;

Expand Down
116 changes: 84 additions & 32 deletions quadratic-multiplayer/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use serde::Serialize;
use tokio::sync::Mutex;
use uuid::Uuid;

use crate::{get_mut_room, get_or_create_room, get_room};

#[derive(Serialize, Debug, Clone)]
pub(crate) struct User {
#[serde(skip_serializing)]
Expand Down Expand Up @@ -61,51 +63,87 @@ impl State {

/// Retrieves a copy of a room.
pub(crate) async fn get_room(&self, file_id: &Uuid) -> Result<Room> {
let rooms = self.rooms.lock().await;
let room = rooms
.get(file_id)
.ok_or(anyhow!("Room {file_id} not found"))?
.to_owned();
let room = get_room!(self, file_id)?.to_owned();

Ok(room)
}

/// Add a user to a room. If the room doesn't exist, it is created. Users
/// are only added to a room once (HashMap).
/// are only added to a room once (HashMap). Returns true if the user was
/// newly added.
pub(crate) async fn enter_room(&self, file_id: Uuid, user: &User) -> bool {
let mut rooms = self.rooms.lock().await;
let room = rooms.entry(file_id).or_insert_with(|| Room::new(file_id));

let user_id = user.id.clone();
let is_new = get_or_create_room!(self, file_id)
.users
.insert(user.id.to_owned(), user.to_owned())
.is_none();

tracing::trace!("User {:?} entered room {:?}", user, room);
tracing::trace!("User {:?} entered room {:?}", user.id, file_id);

Check warning on line 80 in quadratic-multiplayer/src/state.rs

View check run for this annotation

Codecov / codecov/patch

quadratic-multiplayer/src/state.rs#L80

Added line #L80 was not covered by tests

room.users.insert(user_id, user.clone()).is_none()
is_new
}

/// Removes a user from a room. If the room is empty, it deletes the room.
/// Returns true if the room still exists after the user leaves.
pub(crate) async fn leave_room(&self, file_id: Uuid, user_id: &String) -> bool {
let mut rooms = self.rooms.lock().await;

// todo: there's probably a better way of handling the case where the room does not exist
let room = rooms.entry(file_id).or_insert_with(|| Room::new(file_id));
room.users.remove(user_id);

// remove the room if it's empty
if room.users.len() == 0 {
rooms.remove(&file_id);
tracing::trace!(
"User {:?} left room {:?}. Room deleted because it was empty.",
user_id,
file_id
);
false
} else {
tracing::trace!("User {:?} left room {:?}", user_id, room);
true
pub(crate) async fn leave_room(&self, file_id: Uuid, user_id: &String) -> Result<bool> {
get_mut_room!(self, file_id)?.users.remove(user_id);
let num_in_room = get_room!(self, file_id)?.users.len();

tracing::trace!(
"User {:?} leaving room {}, {} left",
user_id,
file_id,
num_in_room
);

Check warning on line 96 in quadratic-multiplayer/src/state.rs

View check run for this annotation

Codecov / codecov/patch

quadratic-multiplayer/src/state.rs#L91-L96

Added lines #L91 - L96 were not covered by tests

if num_in_room == 0 {
self.remove_room(file_id).await;
}

Ok(num_in_room != 0)
}

/// Removes a room.
pub(crate) async fn remove_room(&self, file_id: Uuid) {
self.rooms.lock().await.remove(&file_id);

tracing::trace!("Room {file_id} removed");

Check warning on line 109 in quadratic-multiplayer/src/state.rs

View check run for this annotation

Codecov / codecov/patch

quadratic-multiplayer/src/state.rs#L109

Added line #L109 was not covered by tests
}
}

#[macro_export]
macro_rules! get_room {
( $self:ident, $file_id:ident ) => {
$self
.rooms
.lock()
.await
.get(&$file_id)
.ok_or(anyhow!("Room {} not found", $file_id))
};
}

#[macro_export]
macro_rules! get_mut_room {
( $self:ident, $file_id:ident ) => {
$self
.rooms
.lock()
.await
.get_mut(&$file_id)
.ok_or(anyhow!("Room {} not found", $file_id))
};
}

#[macro_export]
macro_rules! get_or_create_room {
( $self:ident, $file_id:ident ) => {
$self
.rooms
.lock()
.await
.entry($file_id)
.or_insert_with(|| Room::new($file_id))
};
}

#[cfg(test)]
Expand All @@ -115,10 +153,11 @@ mod tests {
use super::*;

#[tokio::test]
async fn enters_and_retrieves_a_room() {
async fn enters_retrieves_and_leaves_a_room() {
let state = State::new();
let file_id = Uuid::new_v4();
let user = new_user();
let user2 = new_user();

let is_new = state.enter_room(file_id, &user).await;
let room = state.get_room(&file_id).await.unwrap();
Expand All @@ -128,5 +167,18 @@ mod tests {
assert_eq!(state.rooms.lock().await.len(), 1);
assert_eq!(room.users.len(), 1);
assert_eq!(room.users.get(&user.id), Some(user));

// leave the room of 2 users
state.enter_room(file_id, &user2).await;
state.leave_room(file_id, &user.id).await.unwrap();
let room = state.get_room(&file_id).await.unwrap();

assert_eq!(room.users.len(), 1);
assert_eq!(room.users.get(&user2.id), Some(&user2));

// leave a room of 1 user
state.leave_room(file_id, &user2.id).await.unwrap();
let room = state.get_room(&file_id).await;
assert!(room.is_err());
}
}
2 changes: 1 addition & 1 deletion quadratic-multiplayer/src/test_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::state::{State, User};

pub(crate) fn new_user() -> User {
User {
id: "user".to_string(),
id: Uuid::new_v4().to_string(),
first_name: FirstName().fake(),
last_name: LastName().fake(),
image: FilePath().fake(),
Expand Down

0 comments on commit 8660be0

Please sign in to comment.