cleanup events + limits + more logs

This commit is contained in:
Theo Browne
2026-02-22 16:23:46 -08:00
parent 43003a5b01
commit c01f3ff076
4 changed files with 90 additions and 23 deletions

View File

@@ -64,10 +64,6 @@ const gameState: GameState = {
type WsData = { ip: string };
const WINDOW_MS = 60_000;
const WS_UPGRADE_LIMIT_PER_MIN = parsePositiveInt(
process.env.WS_UPGRADE_LIMIT_PER_MIN,
20,
);
const HISTORY_LIMIT_PER_MIN = parsePositiveInt(
process.env.HISTORY_LIMIT_PER_MIN,
120,
@@ -221,10 +217,21 @@ function setHistoryCache(key: string, body: string, expiresAt: number) {
const clients = new Set<ServerWebSocket<WsData>>();
function getClientState() {
return {
active: gameState.active,
lastCompleted: gameState.completed.at(-1) ?? null,
scores: gameState.scores,
done: gameState.done,
isPaused: gameState.isPaused,
generation: gameState.generation,
};
}
function broadcast() {
const msg = JSON.stringify({
type: "state",
data: gameState,
data: getClientState(),
totalRounds: runs,
viewerCount: clients.size,
version: VERSION,
@@ -234,6 +241,16 @@ function broadcast() {
}
}
function broadcastViewerCount() {
const msg = JSON.stringify({
type: "viewerCount",
viewerCount: clients.size,
});
for (const ws of clients) {
ws.send(msg);
}
}
function getAdminSnapshot() {
return {
isPaused: gameState.isPaused,
@@ -284,6 +301,7 @@ const server = Bun.serve<WsData>({
});
}
if (isRateLimited(`admin:${ip}`, ADMIN_LIMIT_PER_MIN, WINDOW_MS)) {
log("WARN", "http", "Admin login rate limited", { ip });
return new Response("Too Many Requests", { status: 429 });
}
@@ -467,6 +485,7 @@ const server = Bun.serve<WsData>({
if (url.pathname === "/api/history") {
if (isRateLimited(`history:${ip}`, HISTORY_LIMIT_PER_MIN, WINDOW_MS)) {
log("WARN", "http", "History rate limited", { ip });
return new Response("Too Many Requests", { status: 429 });
}
const rawPage = parseInt(url.searchParams.get("page") || "1", 10);
@@ -514,21 +533,27 @@ const server = Bun.serve<WsData>({
headers: { Allow: "GET" },
});
}
if (
isRateLimited(`ws-upgrade:${ip}`, WS_UPGRADE_LIMIT_PER_MIN, WINDOW_MS)
) {
return new Response("Too Many Requests", { status: 429 });
}
if (clients.size >= MAX_WS_GLOBAL) {
log("WARN", "ws", "Global WS limit reached, rejecting", {
ip,
clients: clients.size,
limit: MAX_WS_GLOBAL,
});
return new Response("Service Unavailable", { status: 503 });
}
const existingForIp = wsByIp.get(ip) ?? 0;
if (existingForIp >= MAX_WS_PER_IP) {
log("WARN", "ws", "Per-IP WS limit reached, rejecting", {
ip,
existing: existingForIp,
limit: MAX_WS_PER_IP,
});
return new Response("Too Many Requests", { status: 429 });
}
const upgraded = server.upgrade(req, { data: { ip } });
if (!upgraded) {
log("WARN", "ws", "WebSocket upgrade failed", { ip });
return new Response("WebSocket upgrade failed", { status: 400 });
}
return undefined;
@@ -540,8 +565,26 @@ const server = Bun.serve<WsData>({
data: {} as WsData,
open(ws) {
clients.add(ws);
wsByIp.set(ws.data.ip, (wsByIp.get(ws.data.ip) ?? 0) + 1);
broadcast();
const ipCount = (wsByIp.get(ws.data.ip) ?? 0) + 1;
wsByIp.set(ws.data.ip, ipCount);
log("INFO", "ws", "Client connected", {
ip: ws.data.ip,
ipConns: ipCount,
totalClients: clients.size,
uniqueIps: wsByIp.size,
});
// Send current state to the new client only
ws.send(
JSON.stringify({
type: "state",
data: getClientState(),
totalRounds: runs,
viewerCount: clients.size,
version: VERSION,
}),
);
// Notify everyone else with just the viewer count
broadcastViewerCount();
},
message(_ws, _message) {
// Spectator-only, no client messages handled
@@ -549,7 +592,12 @@ const server = Bun.serve<WsData>({
close(ws) {
clients.delete(ws);
decrementIpConnection(ws.data.ip);
broadcast();
log("INFO", "ws", "Client disconnected", {
ip: ws.data.ip,
totalClients: clients.size,
uniqueIps: wsByIp.size,
});
broadcastViewerCount();
},
},
development: