diff --git a/backend/src/main/java/com/game/websocket/GameWebSocketHandler.java b/backend/src/main/java/com/game/websocket/GameWebSocketHandler.java index 9abb2b3..34f9161 100644 --- a/backend/src/main/java/com/game/websocket/GameWebSocketHandler.java +++ b/backend/src/main/java/com/game/websocket/GameWebSocketHandler.java @@ -58,6 +58,9 @@ public class GameWebSocketHandler extends TextWebSocketHandler { // Session id -> username, so afterConnectionClosed can clean up without // relying on session attributes surviving an abrupt close private final Map usernameBySessionId = new ConcurrentHashMap<>(); + // Session id -> per-page-load tab id, so a rename-adopting reconnect from + // the same tab can be told apart from a genuine duplicate tab + private final Map tabBySessionId = new ConcurrentHashMap<>(); // Username -> earliest eviction time; entries are cancelled by a reconnect private final Map pendingEvictions = new ConcurrentHashMap<>(); // Per-room ordering for join notifications: without it, two simultaneous @@ -76,6 +79,8 @@ public void afterConnectionEstablished(WebSocketSession rawSession) throws Excep } String username = URLDecoder.decode(rawUsername, StandardCharsets.UTF_8); boolean isStatus = "status".equals(query.get("type")); + String rawTab = query.get("tab"); + String tabId = (rawTab == null || rawTab.isEmpty()) ? "" : URLDecoder.decode(rawTab, StandardCharsets.UTF_8); WebSocketSession session = new ConcurrentWebSocketSessionDecorator( rawSession, SEND_TIME_LIMIT_MS, BUFFER_SIZE_LIMIT_BYTES); @@ -87,17 +92,34 @@ public void afterConnectionEstablished(WebSocketSession rawSession) throws Excep // refresh's old session is already closed, so it keeps its name). WebSocketSession existing = playersByUsername.get(username); if (existing != null && existing.isOpen() && !existing.getId().equals(session.getId())) { - String candidate; - do { - candidate = username + "-" + randomSuffix(); - } while (playersByUsername.containsKey(candidate)); - logger.info("Username {} already live in another tab, assigning {}", username, candidate); - username = candidate; + String existingTabId = tabBySessionId.get(existing.getId()); + if (!tabId.isEmpty() && tabId.equals(existingTabId)) { + // Same tab reconnecting (e.g. right after adopting a + // rename): the old socket just hasn't been torn down yet. + // Keep the name and drop the stale session instead of + // renaming again - closeQuietly below runs before the new + // session is registered, so afterConnectionClosed's + // "still tracked under this id" check still sees the + // stale session as owner and doesn't touch our new one. + logger.info("Username {} reconnecting from same tab {}, dropping stale session", username, tabId); + closeQuietly(existing); + } else { + // Cap growth so repeated duplication can't produce an + // unbounded name + String base = username.length() > 20 ? username.substring(0, 20) : username; + String candidate; + do { + candidate = base + "-" + randomSuffix(); + } while (playersByUsername.containsKey(candidate)); + logger.info("Username {} already live in another tab, assigning {}", username, candidate); + username = candidate; + } } } lastSeen.put(session.getId(), System.currentTimeMillis()); usernameBySessionId.put(session.getId(), username); + tabBySessionId.put(session.getId(), tabId); if (isStatus) { statusSessions.add(session); } else { @@ -219,6 +241,7 @@ private void handleMove(WebSocketSession session, String username, Map s.getId().equals(sessionId)); diff --git a/frontend/src/components/UI/Header.jsx b/frontend/src/components/UI/Header.jsx index 7f4c629..43f45cb 100644 --- a/frontend/src/components/UI/Header.jsx +++ b/frontend/src/components/UI/Header.jsx @@ -42,8 +42,13 @@ const Username = styled.span` font-weight: ${theme.typography.body.fontWeight}; color: ${theme.colors.muted}; font-style: italic; + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; @media (max-width: 768px) { margin-top: ${theme.spacing.xs}; + max-width: 80vw; } `; diff --git a/frontend/src/utils/websocket.js b/frontend/src/utils/websocket.js index 6f461d9..63d7a37 100644 --- a/frontend/src/utils/websocket.js +++ b/frontend/src/utils/websocket.js @@ -25,6 +25,10 @@ class WebSocketService { this.livenessTimer = null; this.lastMessageAt = null; this.pendingAcks = new Map(); // moveId -> { resolve, reject, timeoutId } + // Deliberately in-memory only (per page load): a reconnect of the same + // page keeps this id, while a duplicated tab (which clones sessionStorage) + // gets a fresh one - lets the server tell the two cases apart. + this.tabId = crypto.randomUUID(); } get connected() { @@ -43,7 +47,7 @@ class WebSocketService { const base = isProd ? `${window.location.protocol === 'https:' ? 'wss://' : 'ws://'}${window.location.host}` : 'ws://localhost:8080'; - return `${base}/ws?username=${this.username}`; + return `${base}/ws?username=${this.username}&tab=${this.tabId}`; } _openSocket() {