fix(server): keep matchmaking games out of lobby reports; salvage invalid entries

Ranked matchmaking games have a Public gameType but no publicGameType, so
the worker's lobbyList report included them with the field dropped over
IPC, failing the master's schema parse. The master then discarded the
entire report, freezing its view of that worker's lobbies for the match's
lobby window (~7s): stale player counts and countdowns for all clients,
repeated countdown resets, and duplicate scheduled lobbies.

- Worker: filter games without a publicGameType out of the report;
  matchmaking games are invite-only and must never be advertised.
- Master: validate lobby entries individually and drop only the bad ones
  (logged), so one malformed entry can't invalidate the whole report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
evanpelle
2026-07-18 14:12:29 -07:00
co-authored by Claude Fable 5
parent fe5d7708e0
commit b716adb7e2
4 changed files with 61 additions and 5 deletions
@@ -48,6 +48,33 @@ function startAllWorkers(
return workers;
}
describe("MasterLobbyService lobby report validation", () => {
it("keeps the valid lobbies when a report contains a malformed entry", () => {
const service = createService(1);
const [{ w }] = startAllWorkers(service, 1);
w.emit("message", {
type: "lobbyList",
lobbies: [
// Missing publicGameType, like a matchmaking game reported by
// mistake. It must be dropped without discarding the whole report.
{ gameID: "bad", numClients: 0 },
{ gameID: "good", numClients: 1, publicGameType: "ffa" },
],
});
(service as any).broadcastLobbies();
const broadcast = ((w as any).send as ReturnType<typeof vi.fn>).mock.calls
.map((c) => c[0])
.find((m) => m.type === "lobbiesBroadcast");
expect(broadcast).toBeDefined();
expect(
broadcast.publicGames.games.ffa.map((l: { gameID: string }) => l.gameID),
).toEqual(["good"]);
});
});
describe("MasterLobbyService.isHealthy", () => {
it("unhealthy before any workers register", () => {
const service = createService(4);