myheats

Live heats, scoring and leaderboard for sport events
git clone https://git.in0rdr.ch/myheats.git
Log | Files | Refs | Pull requests |Archive | README | LICENSE

commit 43d574f447e1f9f22a947bf9116586cc2a494a6c
parent 6b31420aea44554db46dcdbe0ba01a3bbe349787
Author: Andreas Gruhler <agruhl@gmx.ch>
Date:   Mon, 16 Mar 2026 17:56:55 +0100

feat(leaderboard): cleanup websocket on component change

This stores the websocket connection in the Leaderboard as a state
variable.

The variable is cleaned up when the component Leaderboard is exited. For
example, when a judge switches to a different tab/component (e.g.,
Scoring), the socket to the server is closed and only recreated when the
tab/component is changed again (to the Leaderboard).

Diffstat:
Msrc/frontend/Leaderboard.jsx | 57+++++++++++++++++++++++++++++++--------------------------
1 file changed, 31 insertions(+), 26 deletions(-)

diff --git a/src/frontend/Leaderboard.jsx b/src/frontend/Leaderboard.jsx @@ -10,21 +10,6 @@ const ws_port = import.meta.env.VITE_WS_PORT const locale = import.meta.env.VITE_LOCALE -// use a socket for the real-time leaderboard data -let socket = new WebSocket(`${ws_uri}:${ws_port}/v1/leaderboard`); -console.info("Attached to server websocket"); - -socket.onclose = async function(event) { - console.info("Server removed us from client list, reattaching socket"); - socket = new WebSocket(`${ws_uri}:${ws_port}/v1/leaderboard`); -} -socket.onopen = function (event) { - // subscribe to scoring from judges when socket is opened - socket.send(JSON.stringify({ - method: "watchScores", - })) -} - export async function addAthleteToHeat(athlete, heat, session) { const res = await fetch(`${api_uri}:${api_port}/v1/leaderboard/addAthleteToHeat`, { method: 'POST', @@ -257,6 +242,7 @@ function NewHeatForm(leaderboard, rankingComp, selectHeatRef, selectRankRef, ses } function Leaderboard({session}) { + const [socket, setSocket] = useState(null) const [loading, setLoading] = useState(false) const [leaderboard, setLeaderboard] = useState([]) const [heatSelection, setHeatSelection] = useState([]) @@ -318,6 +304,26 @@ function Leaderboard({session}) { ] useEffect(() => { + let ws = new WebSocket(`${ws_uri}:${ws_port}/v1/leaderboard`); + console.info("Attached to server websocket"); + + ws.addEventListener("open", event => { + // subscribe to scoring from judges when socket is opened + ws.send(JSON.stringify({ + method: "watchScores", + })) + }); + + setSocket(ws) + + // cleanup websocket when component is changed + return () => { + console.info("Websocket connection closed"); + ws.close(); + } + }, []); + + useEffect(() => { (async () => { setLoading(true) @@ -348,18 +354,17 @@ function Leaderboard({session}) { const scoreSummary = await getScoreSummary(heatSelection.map(h => h.value), session) setLeaderboard(scoreSummary) setLoading(false) - })(); - }, [heatSelection]); - useEffect(() => { - (async() => { - socket.onmessage = async function(event) { - setLoading(true) - - // todo: reload only required scores - const scoreSummary = await getScoreSummary(heatSelection.map(h => h.value), session) - setLeaderboard(scoreSummary) - setLoading(false) + // reload leaderboard on messages from the database (new scores from judges arrived) + if (socket !== null) { + socket.addEventListener("message", async (event) => { + setLoading(true) + + // todo: reload only required scores + const scoreSummary = await getScoreSummary(heatSelection.map(h => h.value), session) + setLeaderboard(scoreSummary) + setLoading(false) + }); } })(); }, [heatSelection]);