diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 5da06e7..262008a 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -717,7 +717,12 @@ wss.on("connection", (socket, req) => { const room = getRoom(roomId); if (!room) return send(socket, { type: "error", message: "no such room" }); const result = joinRoom(room, name, typeof msg.token === "string" ? msg.token : null); - if ("error" in result) return send(socket, { type: "error", message: result.error }); + if ("error" in result) { + // A refused seat is worth a line: a lost seat mid-game is the + // costliest quiet failure this table has. + console.warn(`join refused: room ${room.id} name ${JSON.stringify(name)} — ${result.error}`); + return send(socket, { type: "error", message: result.error }); + } leaveGallery(session); session.playerId = name; session.roomId = room.id; @@ -1065,6 +1070,7 @@ wss.on("connection", (socket, req) => { const result = peekSummary(roomId, name, typeof seat.token === "string" ? seat.token : null); if (result === null) continue; if (result === "badToken") { + console.warn(`seat voided: room ${roomId} name ${JSON.stringify(name)} — the token did not match`); voided.push(`${roomId}:${name}`); continue; } diff --git a/packages/web/src/net.svelte.ts b/packages/web/src/net.svelte.ts index cbc7507..a0f8263 100644 --- a/packages/web/src/net.svelte.ts +++ b/packages/web/src/net.svelte.ts @@ -380,6 +380,8 @@ class Net { feedbackReports = $state([]); /** Every seat this browser holds, across rooms. */ seats = $state(loadSeats()); + /** Seats the last games answer refused: dropped only if refused again. */ + private voidedOnce = new Set(); /** Lobby ledger: one summary per live seat. */ stats = $state | null>(null); chatSeen = $state>(loadChatSeen()); @@ -673,8 +675,13 @@ class Net { // stays — a restarting server, a stale restore, or the wrong // backend all answer with ignorance, and ignorance is not // deletion. The wallet is capped by age instead of by trust. + // And a seat is dropped only on the SECOND refusal in a row: one + // refusal around a restart or a stale answer must not cost a seat. const voided = new Set((msg.voided as string[] | undefined) ?? []); - let kept = this.seats.filter((s) => !voided.has(`${s.roomId}:${s.name}`)); + const doubted = new Set(); + for (const key of voided) { if (this.voidedOnce.has(key)) doubted.add(key); } + this.voidedOnce = voided; + let kept = this.seats.filter((s) => !doubted.has(`${s.roomId}:${s.name}`)); if (kept.length > 50) kept = kept.slice(kept.length - 50); if (kept.length !== this.seats.length) { this.seats = kept;