lobby websocket instead of polling (#2727)

## Description:
Changes game lobbies into websockets instead of polling

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

w.o.n

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: iamlewis <lewismmmm@gmail.com>
This commit is contained in:
Ryan
2026-01-01 17:38:33 +00:00
committed by GitHub
parent 9d5f167446
commit 3dcd38a58d
5 changed files with 378 additions and 64 deletions
+62 -5
View File
@@ -5,6 +5,7 @@ import rateLimit from "express-rate-limit";
import http from "http";
import path from "path";
import { fileURLToPath } from "url";
import { WebSocket, WebSocketServer } from "ws";
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
import { GameInfo } from "../core/Schemas";
import { generateID } from "../core/Util";
@@ -59,9 +60,32 @@ app.use(
}),
);
let publicLobbiesJsonStr = "";
let publicLobbiesData: { lobbies: GameInfo[] } = { lobbies: [] };
const publicLobbyIDs: Set<string> = new Set();
const connectedClients: Set<WebSocket> = new Set();
// Broadcast lobbies to all connected clients
function broadcastLobbies() {
const message = JSON.stringify({
type: "lobbies_update",
data: publicLobbiesData,
});
const clientsToRemove: WebSocket[] = [];
connectedClients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
} else {
clientsToRemove.push(client);
}
});
clientsToRemove.forEach((client) => {
connectedClients.delete(client);
});
}
// Start the master process
export async function startMaster() {
@@ -74,6 +98,37 @@ export async function startMaster() {
log.info(`Primary ${process.pid} is running`);
log.info(`Setting up ${config.numWorkers()} workers...`);
// Setup WebSocket server for clients
const wss = new WebSocketServer({ server, path: "/lobbies" });
wss.on("connection", (ws: WebSocket) => {
connectedClients.add(ws);
// Send current lobbies immediately (always send, even if empty)
ws.send(
JSON.stringify({ type: "lobbies_update", data: publicLobbiesData }),
);
ws.on("close", () => {
connectedClients.delete(ws);
});
ws.on("error", (error) => {
log.error(`WebSocket error:`, error);
connectedClients.delete(ws);
try {
if (
ws.readyState === WebSocket.OPEN ||
ws.readyState === WebSocket.CONNECTING
) {
ws.close(1011, "WebSocket internal error");
}
} catch (closeError) {
log.error("Error while closing WebSocket after error:", closeError);
}
});
});
// Generate admin token for worker authentication
const ADMIN_TOKEN = crypto.randomBytes(16).toString("hex");
process.env.ADMIN_TOKEN = ADMIN_TOKEN;
@@ -158,7 +213,7 @@ app.get("/api/env", async (req, res) => {
// Add lobbies endpoint to list public games for this worker
app.get("/api/public_lobbies", async (req, res) => {
res.send(publicLobbiesJsonStr);
res.json(publicLobbiesData);
});
async function fetchLobbies(): Promise<number> {
@@ -225,10 +280,12 @@ async function fetchLobbies(): Promise<number> {
}
});
// Update the JSON string
publicLobbiesJsonStr = JSON.stringify({
// Update the lobbies data
publicLobbiesData = {
lobbies: lobbyInfos,
});
};
broadcastLobbies();
return publicLobbyIDs.size;
}