fix(frontend): scope player identity to a tab, not the browser
Two tabs of the same browser share localStorage, so both got the same persisted playerId and stepped on each other: matchmaking's waiting queue is keyed by playerId and dedupes a second queue:join as a no-op, so two tabs searching never both count as waiting; and re-identifying with the same playerId hijacks the ws registry's socket binding, tearing down the first tab's connection (surfacing as room:join replying 'room already has two players' once its creator's socket is dead). Move playerId/name from localStorage to sessionStorage: still stable across reloads within a tab (reconnect-by-refresh keeps the same identity), but each new tab gets a fresh UUID, so two tabs of one browser behave like two separate players. Not IP-related; no such restriction existed.
This commit is contained in:
parent
16d36328ec
commit
42d1732816
2 changed files with 15 additions and 10 deletions
|
|
@ -25,8 +25,10 @@ message-specific fields — no envelope wrapper, no request/response ids.
|
||||||
|
|
||||||
There is no account system and none is planned — adding auth is out of
|
There is no account system and none is planned — adding auth is out of
|
||||||
scope for multiplayer entirely. Instead, the client generates a
|
scope for multiplayer entirely. Instead, the client generates a
|
||||||
`playerId` (a `crypto.randomUUID()` v4 string) the first time it loads,
|
`playerId` (a `crypto.randomUUID()` v4 string) the first time a tab
|
||||||
persists it in `localStorage`, and reuses it on every future visit. The
|
loads, persists it in `sessionStorage`, and reuses it for the lifetime
|
||||||
|
of that tab (a reload keeps the same identity; a new tab gets a new
|
||||||
|
one, so two tabs of the same browser act as two distinct players). The
|
||||||
player also picks/keeps a display `name` (also persisted client-side, no
|
player also picks/keeps a display `name` (also persisted client-side, no
|
||||||
server-side uniqueness check). The very first message a client sends
|
server-side uniqueness check). The very first message a client sends
|
||||||
after the socket opens registers both with the server:
|
after the socket opens registers both with the server:
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,21 @@
|
||||||
// Per-browser player identity. There is no account system (see
|
// Per-tab player identity. There is no account system (see
|
||||||
// backend/src/PROTOCOL.md's "Identity" section) — just a stable UUID
|
// backend/src/PROTOCOL.md's "Identity" section) — just a stable UUID
|
||||||
// generated once and reused, plus a freely editable display name. Both
|
// generated once and reused, plus a freely editable display name. Both
|
||||||
// are persisted in localStorage so they survive reloads and reconnects.
|
// are persisted in sessionStorage so they survive reloads/reconnects
|
||||||
|
// within a tab but don't leak across tabs — two tabs of the same
|
||||||
|
// browser (e.g. testing matchmaking/private rooms locally) are two
|
||||||
|
// distinct players, the same as two separate browsers would be.
|
||||||
|
|
||||||
const PLAYER_ID_KEY = 'themeGuess.playerId';
|
const PLAYER_ID_KEY = 'themeGuess.playerId';
|
||||||
const PLAYER_NAME_KEY = 'themeGuess.playerName';
|
const PLAYER_NAME_KEY = 'themeGuess.playerName';
|
||||||
|
|
||||||
/** Returns the persisted `playerId`, generating and storing a new
|
/** Returns the persisted `playerId`, generating and storing a new
|
||||||
* `crypto.randomUUID()` the first time this browser is seen. */
|
* `crypto.randomUUID()` the first time this tab is seen. */
|
||||||
export function getOrCreatePlayerId(): string {
|
export function getOrCreatePlayerId(): string {
|
||||||
const existing = localStorage.getItem(PLAYER_ID_KEY);
|
const existing = sessionStorage.getItem(PLAYER_ID_KEY);
|
||||||
if (existing) return existing;
|
if (existing) return existing;
|
||||||
const created = crypto.randomUUID();
|
const created = crypto.randomUUID();
|
||||||
localStorage.setItem(PLAYER_ID_KEY, created);
|
sessionStorage.setItem(PLAYER_ID_KEY, created);
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20,14 +23,14 @@ export function getOrCreatePlayerId(): string {
|
||||||
* a generated default derived from the player's id so `identify` never
|
* a generated default derived from the player's id so `identify` never
|
||||||
* has to send an empty `name`. */
|
* has to send an empty `name`. */
|
||||||
export function getPlayerName(): string {
|
export function getPlayerName(): string {
|
||||||
const existing = localStorage.getItem(PLAYER_NAME_KEY);
|
const existing = sessionStorage.getItem(PLAYER_NAME_KEY);
|
||||||
if (existing) return existing;
|
if (existing) return existing;
|
||||||
const fallback = `Player${getOrCreatePlayerId().slice(0, 4)}`;
|
const fallback = `Player${getOrCreatePlayerId().slice(0, 4)}`;
|
||||||
localStorage.setItem(PLAYER_NAME_KEY, fallback);
|
sessionStorage.setItem(PLAYER_NAME_KEY, fallback);
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Overwrites the persisted display name. */
|
/** Overwrites the persisted display name. */
|
||||||
export function setPlayerName(name: string): void {
|
export function setPlayerName(name: string): void {
|
||||||
localStorage.setItem(PLAYER_NAME_KEY, name);
|
sessionStorage.setItem(PLAYER_NAME_KEY, name);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue