Summary Update.
This commit is contained in:
@@ -2,12 +2,32 @@ using System.Collections.Concurrent;
|
||||
|
||||
namespace RelayServer.Services.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Two-way in-memory mapping between WebSocket session IDs and usernames.
|
||||
///
|
||||
/// Why both directions: when a chat message arrives, we need to look up "which sessions does
|
||||
/// this server member have open right now?" (username → sessions) so we can deliver to each
|
||||
/// of their devices. When a connection closes, we need to know "which user owned this session?"
|
||||
/// (session → username) to clean up correctly.
|
||||
///
|
||||
/// Multi-device support: one username can have multiple sessions (phone + desktop + web all
|
||||
/// connected simultaneously). UsernameToSessions stores a HashSet per username; each lock
|
||||
/// is scoped to that specific HashSet so different users never block each other.
|
||||
///
|
||||
/// Username comparisons are case-insensitive (OrdinalIgnoreCase on the outer dictionary)
|
||||
/// because the DB stores usernames lowercase but clients may register with mixed case.
|
||||
/// </summary>
|
||||
public static class ConnectedClientService
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, string> SessionToUsername = new();
|
||||
private static readonly ConcurrentDictionary<string, HashSet<string>> UsernameToSessions =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Associates a session ID with a username. Called from HandleRegisterKey. If the same
|
||||
/// session re-registers under a different username (rare — basically only if the client
|
||||
/// reauthenticates), the old mapping is cleaned up first to avoid double-bookkeeping.
|
||||
/// </summary>
|
||||
public static void Register(string sessionId, string username)
|
||||
{
|
||||
if (SessionToUsername.TryGetValue(sessionId, out var oldUsername) &&
|
||||
@@ -26,12 +46,21 @@ public static class ConnectedClientService
|
||||
sessions.Add(sessionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a session from both mappings. Called from OnClose. Idempotent — calling for
|
||||
/// a session that's already gone is a no-op.
|
||||
/// </summary>
|
||||
public static void Unregister(string sessionId)
|
||||
{
|
||||
if (SessionToUsername.TryRemove(sessionId, out var username))
|
||||
RemoveSessionFromUsername(sessionId, username);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns every active session ID for a given username (case-insensitive lookup).
|
||||
/// Empty collection if the user is offline. Snapshot-safe: the returned list is a copy,
|
||||
/// not a live view of the underlying HashSet.
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<string> GetSessionsForUser(string username)
|
||||
{
|
||||
if (UsernameToSessions.TryGetValue(username, out var sessions))
|
||||
@@ -43,11 +72,19 @@ public static class ConnectedClientService
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reverse lookup: which user owns this session? Returns the mixed-case username the
|
||||
/// client registered with (preserves casing for display). Null if the session is unknown.
|
||||
/// </summary>
|
||||
public static string? GetUsernameForSession(string sessionId)
|
||||
{
|
||||
return SessionToUsername.TryGetValue(sessionId, out var u) ? u : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal cleanup: pulls a session out of the username→sessions HashSet, and removes
|
||||
/// the username entry entirely if no sessions remain (keeps the dictionary lean).
|
||||
/// </summary>
|
||||
private static void RemoveSessionFromUsername(string sessionId, string username)
|
||||
{
|
||||
if (!UsernameToSessions.TryGetValue(username, out var sessions))
|
||||
|
||||
Reference in New Issue
Block a user