feat: remove the need for constant polling

This commit is contained in:
2026-03-27 13:09:35 -04:00
Unverified
parent c8781fddaa
commit 77724ba260
2 changed files with 66 additions and 18 deletions

View File

@@ -51,7 +51,6 @@ export default function SpectatePage() {
const [selectedGame, setSelectedGame] = useState<number | null>(null); const [selectedGame, setSelectedGame] = useState<number | null>(null);
const [log, setLog] = useState<string[]>([]); const [log, setLog] = useState<string[]>([]);
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
const liveGamesRef = useRef<Map<number, LiveGame>>(new Map()); const liveGamesRef = useRef<Map<number, LiveGame>>(new Map());
const addLog = useCallback( const addLog = useCallback(
@@ -120,11 +119,48 @@ export default function SpectatePage() {
break; break;
case "TOURNAMENT_END": case "TOURNAMENT_END":
setTournamentActive(false);
setTournamentType(null);
addLog("Round ended"); addLog("Round ended");
send(cmd.gameList()); send(cmd.gameList());
send(cmd.playerList()); send(cmd.playerList());
break; break;
case "CONNECT_EVENT":
addLog(`Player joined: ${msg.username}`);
send(cmd.playerList());
break;
case "DISCONNECT_EVENT":
addLog(`Player left: ${msg.username}`);
send(cmd.playerList());
send(cmd.gameList());
break;
case "READY_EVENT": {
let found = false;
setPlayers((prev) =>
prev.map((player) => {
if (player.username !== msg.username) return player;
found = true;
return { ...player, ready: msg.ready };
}),
);
if (!found) {
send(cmd.playerList());
}
break;
}
case "GAME_MATCH_START":
addLog(
`Match started: #${msg.matchId} ${msg.player1} vs ${msg.player2}`,
);
send(cmd.gameList());
send(cmd.playerList());
break;
case "GAME_LIST": case "GAME_LIST":
setGameList(msg.games); setGameList(msg.games);
for (const g of msg.games) { for (const g of msg.games) {
@@ -228,29 +264,14 @@ export default function SpectatePage() {
}); });
return unsubscribe; return unsubscribe;
}, [addLog, selectedGame, send, subscribe, updateGame]); }, [addLog, send, subscribe, updateGame]);
useEffect(() => { useEffect(() => {
if (status !== "connected" || role !== "observer") { if (status !== "connected" || role !== "observer") return;
if (pollRef.current) {
clearInterval(pollRef.current);
pollRef.current = null;
}
return;
}
send(cmd.getData("TOURNAMENT_STATUS")); send(cmd.getData("TOURNAMENT_STATUS"));
send(cmd.gameList()); send(cmd.gameList());
send(cmd.playerList()); send(cmd.playerList());
pollRef.current = setInterval(() => {
send(cmd.gameList());
send(cmd.playerList());
}, 5000);
return () => {
if (pollRef.current) clearInterval(pollRef.current);
};
}, [role, send, status]); }, [role, send, status]);
const selectedGameData = const selectedGameData =

View File

@@ -33,11 +33,20 @@ export const RECONNECT_TIMEOUT_MS = 60000;
export type ParsedMessage = export type ParsedMessage =
| { type: "CONNECT_ACK" } | { type: "CONNECT_ACK" }
| { type: "CONNECT_EVENT"; username: string }
| { type: "RECONNECT_ACK" } | { type: "RECONNECT_ACK" }
| { type: "DISCONNECT_ACK" } | { type: "DISCONNECT_ACK" }
| { type: "DISCONNECT_EVENT"; username: string }
| { type: "OBSERVE_ACK"; enabled: boolean } | { type: "OBSERVE_ACK"; enabled: boolean }
| { type: "READY_ACK" } | { type: "READY_ACK" }
| { type: "READY_EVENT"; username: string; ready: boolean }
| { type: "GAME_START"; goesFirst: boolean } | { type: "GAME_START"; goesFirst: boolean }
| {
type: "GAME_MATCH_START";
matchId: number;
player1: string;
player2: string;
}
| { type: "GAME_WINS" } | { type: "GAME_WINS" }
| { type: "GAME_LOSS" } | { type: "GAME_LOSS" }
| { type: "GAME_DRAW"; matchId?: number } | { type: "GAME_DRAW"; matchId?: number }
@@ -72,12 +81,14 @@ export function parseMessage(raw: string): ParsedMessage {
switch (parts[0]) { switch (parts[0]) {
case "CONNECT": case "CONNECT":
if (parts[1] === "ACK") return { type: "CONNECT_ACK" }; if (parts[1] === "ACK") return { type: "CONNECT_ACK" };
return { type: "CONNECT_EVENT", username: parts[1] ?? "" };
break; break;
case "RECONNECT": case "RECONNECT":
if (parts[1] === "ACK") return { type: "RECONNECT_ACK" }; if (parts[1] === "ACK") return { type: "RECONNECT_ACK" };
break; break;
case "DISCONNECT": case "DISCONNECT":
if (parts[1] === "ACK") return { type: "DISCONNECT_ACK" }; if (parts[1] === "ACK") return { type: "DISCONNECT_ACK" };
return { type: "DISCONNECT_EVENT", username: parts[1] ?? "" };
break; break;
case "OBSERVE": case "OBSERVE":
if (parts[1] === "ACK") { if (parts[1] === "ACK") {
@@ -86,6 +97,13 @@ export function parseMessage(raw: string): ParsedMessage {
break; break;
case "READY": case "READY":
if (parts[1] === "ACK") return { type: "READY_ACK" }; if (parts[1] === "ACK") return { type: "READY_ACK" };
if (parts.length >= 3) {
return {
type: "READY_EVENT",
username: parts[1],
ready: parts[2] === "true",
};
}
break; break;
case "GAME": { case "GAME": {
@@ -114,6 +132,15 @@ export function parseMessage(raw: string): ParsedMessage {
switch (parts[1]) { switch (parts[1]) {
case "START": case "START":
if (parts[2]?.includes(",")) {
const [matchId, player1, player2] = parts[2].split(",");
return {
type: "GAME_MATCH_START",
matchId: parseInt(matchId, 10),
player1,
player2,
};
}
return { type: "GAME_START", goesFirst: parts[2] === "1" }; return { type: "GAME_START", goesFirst: parts[2] === "1" };
case "WINS": case "WINS":
return { type: "GAME_WINS" }; return { type: "GAME_WINS" };