Reported directly from the container's crash output: TypeError: Cannot
read properties of undefined (reading 'isPaused') inside SocketGateway's
"playstate" handler, taking down the entire Node process - every active
party, not just one connection.
Root cause: party.members is keyed purely by userId, with no per-device
identity yet (the gap the "same account, different device" feature also
needs - see docs/architecture.md). Two connections for the same userId
share one map slot; PartyService.join/create both unconditionally
overwrite it. When the connection currently occupying that slot is the
party's last member and leaves, leave() deletes the entry and - by
design, since there's no successor - never reassigns the now-stale
party.hostUserId. The OTHER connection for that same userId, still fully
connected with its own closure referencing the same now-destroyed Party
object, sends its regular 3s heartbeat next: the host check passes
(stale userId equals stale hostUserId on both sides), then
party.members.get(userId)! finds nothing at all. That's the crash.
Fixed at two levels:
- server/src/realtime/SocketGateway.ts: the "playstate" handler's
unchecked `!` is now a real `if (!member) return`, matching the guard
every other handler already used for the same lookup.
- Every socket.on(...) in attach() now goes through a new safe()
wrapper (log-and-swallow on an unexpected exception) - Socket.IO has
no equivalent of Express's error-handling middleware (asyncRoute()
already covers every REST route for exactly this), so a synchronous
throw from any listener, this one or a future one, would otherwise
keep crashing the whole process outright.
- server/test/socketGateway.test.ts: a regression test reproducing the
exact sequence. Verified it's meaningful, not vacuous, by temporarily
reverting just the specific guard and confirming that same test
correctly fails before restoring the fix.
Deliberately not changing how join/leave share that one map slot in this
pass - that's the real design work the fuller device-detection feature
still needs. This stops it from being able to crash the server in the
meantime, regardless of how that gets resolved.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>