lobby counts

This commit is contained in:
evanpelle
2026-03-14 12:06:45 -07:00
parent f6167d2d94
commit 14077332b3
4 changed files with 80 additions and 8 deletions
+30 -5
View File
@@ -1,5 +1,9 @@
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import { PublicGames, PublicGamesSchema } from "../core/Schemas";
import {
LobbyCountsSchema,
PublicGames,
PublicGamesSchema,
} from "../core/Schemas";
interface LobbySocketOptions {
reconnectDelay?: number;
@@ -19,6 +23,7 @@ export class PublicLobbySocket {
private wsAttemptCounted = false;
private workerPath: string = "";
private stopped = true;
private lastGames: PublicGames | null = null;
private readonly reconnectDelay: number;
private readonly maxWsAttempts: number;
@@ -79,10 +84,30 @@ export class PublicLobbySocket {
private handleMessage(event: MessageEvent) {
try {
const publicGames = PublicGamesSchema.parse(
JSON.parse(event.data as string),
);
this.onLobbiesUpdate(publicGames);
const raw = JSON.parse(event.data as string);
// Slim update — just counts/timers, merge into cached full state.
const counts = LobbyCountsSchema.safeParse(raw);
if (counts.success && this.lastGames) {
const games = {} as PublicGames["games"];
for (const [type, lobbies] of Object.entries(this.lastGames.games) as [
keyof PublicGames["games"],
PublicGames["games"][keyof PublicGames["games"]],
][]) {
games[type] = lobbies.map((lobby) => {
const update = counts.data.counts[lobby.gameID];
return update ? { ...lobby, ...update } : lobby;
}) as PublicGames["games"][typeof type];
}
this.onLobbiesUpdate({ serverTime: counts.data.serverTime, games });
this.lastGames = { serverTime: counts.data.serverTime, games };
return;
}
// Full update — store and forward.
const incoming = PublicGamesSchema.parse(raw);
this.lastGames = incoming;
this.onLobbiesUpdate(incoming);
} catch (error) {
console.error("Error parsing WebSocket message:", error);
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
+10
View File
@@ -169,6 +169,16 @@ export const PublicGamesSchema = z.object({
games: z.record(PublicGameTypeSchema, z.array(PublicGameInfoSchema)),
});
// Slim update sent when game configs haven't changed — just player counts + timers.
export const LobbyCountsSchema = z.object({
serverTime: z.number(),
counts: z.record(
z.string(), // gameID
z.object({ numClients: z.number(), startsAt: z.number().optional() }),
),
});
export type LobbyCounts = z.infer<typeof LobbyCountsSchema>;
export class LobbyInfoEvent implements GameEvent {
constructor(
public lobby: GameInfo,
+1 -1
View File
@@ -75,7 +75,7 @@ export class MasterLobbyService {
if (this.readyWorkers.size === this.config.numWorkers() && !this.started) {
this.started = true;
this.log.info("All workers ready, starting game scheduling");
startPolling(async () => this.broadcastLobbies(), 500);
startPolling(async () => this.broadcastLobbies(), 250);
startPolling(async () => await this.maybeScheduleLobby(), 1000);
}
}
+39 -2
View File
@@ -1,6 +1,6 @@
import http from "http";
import { WebSocket, WebSocketServer } from "ws";
import { PublicGameInfo, PublicGames } from "../core/Schemas";
import { LobbyCounts, PublicGameInfo, PublicGames } from "../core/Schemas";
import { GameManager } from "./GameManager";
import {
MasterMessageSchema,
@@ -12,6 +12,8 @@ import { logger } from "./Logger";
export class WorkerLobbyService {
private readonly lobbiesWss: WebSocketServer;
private readonly lobbyClients: Set<WebSocket> = new Set();
private lastPublicGames: PublicGames | null = null;
private lastGameSetKey: string = "";
constructor(
private readonly server: http.Server,
@@ -112,6 +114,10 @@ export class WorkerLobbyService {
private setupLobbiesWebSocket() {
this.lobbiesWss.on("connection", (ws: WebSocket) => {
this.lobbyClients.add(ws);
// Send the last known full state immediately on connect.
if (this.lastPublicGames && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(this.lastPublicGames));
}
ws.on("message", () => {
ws.terminate();
});
@@ -136,8 +142,39 @@ export class WorkerLobbyService {
});
}
private buildCountsMessage(publicGames: PublicGames): string {
const counts: LobbyCounts["counts"] = {};
for (const lobbies of Object.values(publicGames.games)) {
for (const lobby of lobbies) {
counts[lobby.gameID] = {
numClients: lobby.numClients,
startsAt: lobby.startsAt,
};
}
}
return JSON.stringify({
serverTime: publicGames.serverTime,
counts,
} satisfies LobbyCounts);
}
private broadcastLobbiesToClients(publicGames: PublicGames) {
const message = JSON.stringify(publicGames);
const allLobbies = Object.values(publicGames.games).flat();
const gameSetKey = allLobbies
.map((g) => g.gameID)
.sort()
.join(",");
const configsChanged = gameSetKey !== this.lastGameSetKey;
this.lastGameSetKey = gameSetKey;
if (configsChanged) {
this.lastPublicGames = publicGames;
}
const message = configsChanged
? JSON.stringify(publicGames)
: this.buildCountsMessage(publicGames);
const clientsToRemove: WebSocket[] = [];
this.lobbyClients.forEach((client) => {