Implement session replacement and WebSocket keepalive features

Enhanced the session management by allowing a reconnect with the same username to replace the existing session, sending a logout message to the previous session. Introduced WebSocket keepalive functionality using Ping/Pong messages to detect stale connections. Updated documentation to reflect these changes and improve user experience during reconnections.
This commit is contained in:
Torsten Schulz (local)
2026-03-05 08:03:15 +01:00
parent 92ae7d614e
commit 8b9947cc03
5 changed files with 174 additions and 112 deletions

View File

@@ -211,83 +211,95 @@ async fn handle_init_command(
return;
}
let (token, user_name, actual_room_name, old_room_name) = {
let mut guard = state.write().await;
if guard.logged_in_names.contains(&requested_user_name)
&& guard
.clients
.iter()
.any(|(id, c)| *id != client_id && c.logged_in && c.user_name == requested_user_name)
{
drop(guard);
send_error(client_id, Arc::clone(&state), "loggedin").await;
return;
}
if !guard.room_meta.contains_key(&resolved_room_name) {
drop(guard);
if room_debug_enabled() {
eprintln!(
"[yourchat2][room-debug][init] client_id={client_id} resolved_room='{resolved_room_name}' vanished_before_join"
);
let (token, user_name, actual_room_name, old_room_name) = loop {
let replacement_needed = {
let mut guard = state.write().await;
if let Some(existing_client_id) = guard.clients.iter().find_map(|(id, c)| {
if *id != client_id && c.logged_in && c.user_name == requested_user_name {
Some(*id)
} else {
None
}
}) {
Some(existing_client_id)
} else {
if !guard.room_meta.contains_key(&resolved_room_name) {
drop(guard);
if room_debug_enabled() {
eprintln!(
"[yourchat2][room-debug][init] client_id={client_id} resolved_room='{resolved_room_name}' vanished_before_join"
);
}
send_error(client_id, Arc::clone(&state), "room_not_found").await;
return;
}
let (old_room, old_name, was_logged_in, user_name, token, new_token) = {
let Some(client) = guard.clients.get_mut(&client_id) else {
return;
};
let old_room = client.room.clone();
let old_name = client.user_name.clone();
let was_logged_in = client.logged_in;
client.user_name = profile.display_name.clone();
client.color = profile.color.clone();
client.falukant_user_id = profile.falukant_user_id;
client.chat_user_id = profile.chat_user_id;
client.gender_id = profile.gender_id;
client.age = profile.age;
client.rights = profile.rights.clone();
client.right_type_ids = profile.right_type_ids.clone();
client.logged_in = true;
client.room = resolved_room_name.clone();
let mut new_token = None;
if client.token.is_none() {
let generated = Uuid::new_v4().to_string();
client.token = Some(generated.clone());
new_token = Some(generated);
}
(
old_room,
old_name,
was_logged_in,
client.user_name.clone(),
client.token.clone().unwrap_or_default(),
new_token,
)
};
if let Some(generated) = new_token {
guard.tokens.insert(generated, client_id);
}
if was_logged_in {
guard.logged_in_names.remove(&old_name);
}
guard.logged_in_names.insert(user_name.clone());
if !old_room.is_empty() {
if let Some(members) = guard.rooms.get_mut(&old_room) {
members.remove(&client_id);
}
}
guard
.rooms
.entry(resolved_room_name.clone())
.or_default()
.insert(client_id);
break (token, user_name, resolved_room_name.clone(), old_room);
}
send_error(client_id, Arc::clone(&state), "room_not_found").await;
return;
}
let (old_room, old_name, was_logged_in, user_name, token, new_token) = {
let Some(client) = guard.clients.get_mut(&client_id) else {
return;
};
let old_room = client.room.clone();
let old_name = client.user_name.clone();
let was_logged_in = client.logged_in;
client.user_name = profile.display_name.clone();
client.color = profile.color.clone();
client.falukant_user_id = profile.falukant_user_id;
client.chat_user_id = profile.chat_user_id;
client.gender_id = profile.gender_id;
client.age = profile.age;
client.rights = profile.rights.clone();
client.right_type_ids = profile.right_type_ids.clone();
client.logged_in = true;
client.room = resolved_room_name.clone();
let mut new_token = None;
if client.token.is_none() {
let generated = Uuid::new_v4().to_string();
client.token = Some(generated.clone());
new_token = Some(generated);
}
(
old_room,
old_name,
was_logged_in,
client.user_name.clone(),
client.token.clone().unwrap_or_default(),
new_token,
)
};
if let Some(generated) = new_token {
guard.tokens.insert(generated, client_id);
if let Some(old_client_id) = replacement_needed {
state::send_to_client(
old_client_id,
Arc::clone(&state),
json!({"type":5, "message":"logout", "reason":"replaced_by_new_login"}),
)
.await;
state::disconnect_client(old_client_id, Arc::clone(&state)).await;
continue;
}
if was_logged_in {
guard.logged_in_names.remove(&old_name);
}
guard.logged_in_names.insert(user_name.clone());
if !old_room.is_empty() {
if let Some(members) = guard.rooms.get_mut(&old_room) {
members.remove(&client_id);
}
}
guard
.rooms
.entry(resolved_room_name.clone())
.or_default()
.insert(client_id);
(token, user_name, resolved_room_name, old_room)
};
if !old_room_name.is_empty() {
state::mark_room_possibly_empty(&old_room_name, Arc::clone(&state)).await;