On disconnect, a player's slot is held for 60s via ReconnectManager.
During the grace window, game.delta frames destined for the absent
slot are buffered in order. If the client reconnects with its
original token (envelope-level), the grace timer is cancelled and
the server replays a fresh game.state snapshot plus every buffered
delta. Timer expiry triggers the original "player_left" game.end
cleanup that previously ran immediately on disconnect.
- new packages/server/src/reconnect.ts: ReconnectManager (no WS refs,
no registry coupling, unref'd timers so tests don't block exit)
- broadcast.ts: unregisterConnection starts grace; handleRoomJoin
routes reconnect path via envelope token + isPending check;
handleGameMove buffers deltas for disconnected opponents
- reconnect.test.ts: 9 unit cases (grace/cancel/buffer/expire/reset)
- broadcast.test.ts: end-to-end reconnect scenario + negative case