3 Commits
Author SHA1 Message Date
tiennm99 55abdb36c1 refactor(wsapi): dedupe routing and seating, fix ordering races
Route room inputs through one toRoom helper, iterate live seats with
connected(), and share seating and reconnect-window code. Declare every
error code once in errcodes.go, which the web vocabulary test now reads.

Fixes:
- a kicked player is released before being told, so their next action
  is answered not_in_a_room
- a rejected word is counted before move_rejected is sent
- room code draw and registration share one lock hold
- two concurrent resumes with one token can no longer both take a seat
- multiplayer tests ready guests one at a time instead of racing

Behaviour changes:
- opening or joining another room while a game is running is refused
  with already_in_a_game
- resigning in the lobby answers game_not_started
2026-09-28 15:19:59 +07:00
tiennm99 8b3e8f7e67 fix(wsapi): guard room seating races, refuse games during drain, and drop the history copy
A session can tear down between the hub handing a room its seating
message and the room goroutine draining it off the queue. Nothing else
ever learns that, since leaveRoom only notifies a room the session had
already attached to. Left seated as connected, allConnected() could
report true and let quick match auto-start a game against a dead
socket. handleCreate, handleJoin, handleStartBot and handleResume now
check the connection's context and either reopen the grace window the
disconnect would have, or cancel the room when there is no lobby left
to hold one open.

beginGame could also raise the live-game count after the drain
decision: an existing lobby's StartGame, and quick match's own
auto-start, now refuse with server_restarting once the hub is
draining, matching the refusal newRegisteredRoom already gives a
brand-new room.

Engine.Snapshot() copied the whole move history on every broadcast for
two callers that only ever wanted the last move or the played-word
set. Engine.LastMove and Engine.UsedWords answer both directly off the
engine's own state, State.History is gone, and Standings is only
computed once the game is actually over, which is the only time it is
meaningful.
2026-09-21 16:13:52 +07:00
tiennm99 8223f40b16 feat(server): counters, readiness, drain mode and a version stamp
expvar counters for connections, rooms, games, submissions by rejection
reason, eliminations, chat, joins and bot moves, served on a separate
debug address so they never sit on the public mux, plus one structured
word_rejected log line per refused word carrying the normalized word and
its link. GET /readyz flips to 503 while draining; SIGTERM stops new
rooms, waits up to NOITU_DRAIN_TIMEOUT for live games, then shuts down.
GET /version and the startup log carry the build's git describe.

Fuzz targets for the frame decoder, the text sanitizer and Vietnamese
normalization; the last one found that composing before lowercasing
could leave a non-NFC result, now recomposed after lowering.

CI runs on dev as well as main, gates gofmt and golangci-lint, tracks the
buf major instead of an exact pin, and dependabot watches every
ecosystem. The lint findings that had been hidden by the default
per-issue cap are fixed.
2026-09-21 01:17:52 +07:00