From a2ac900f3ec5c04d697482b4649f5884466ca421 Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 13 Mar 2026 15:47:22 -0600 Subject: [PATCH] fixed race condition on web socket status updates --- src/pages/Devices.tsx | 48 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index 08fb013..0a6b9af 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -32,6 +32,28 @@ import { cloneDeep } from "lodash"; import LoadingScreen from "app/LoadingScreen"; import TeamDialog from "teams/TeamDialog"; +type TimestampedReading = { + timestamp?: string; +}; + +const getTimestampMillis = (timestamp?: string): number | undefined => { + if (!timestamp) return undefined; + const parsed = Date.parse(timestamp); + return Number.isNaN(parsed) ? undefined : parsed; +}; + +const pickNewestReading = ( + current: T | null | undefined, + incoming: T | null | undefined +): T | null | undefined => { + const currentTs = getTimestampMillis(current?.timestamp); + const incomingTs = getTimestampMillis(incoming?.timestamp); + + if (incomingTs === undefined) return current ?? incoming; + if (currentTs === undefined) return incoming ?? current; + return incomingTs >= currentTs ? incoming : current; +}; + const useStyles = makeStyles((theme: Theme) => { return ({ buttonBox: { @@ -212,7 +234,31 @@ export default function Devices() { prev.map((d) => { if (d.id().toString() !== deviceId) return d const updated = Device.clone(d) - updated.status = status + const incomingStatus = pond.DeviceStatus.fromObject(status) + const currentStatus = d.status ?? pond.DeviceStatus.create() + + // Guard against out-of-order websocket events by keeping whichever + // reading has the newest timestamp for each independently-updated sensor. + incomingStatus.plenum = pickNewestReading(currentStatus.plenum, incomingStatus.plenum) + incomingStatus.sen5x = pickNewestReading(currentStatus.sen5x, incomingStatus.sen5x) + incomingStatus.co = pickNewestReading(currentStatus.co, incomingStatus.co) + incomingStatus.co2 = pickNewestReading(currentStatus.co2, incomingStatus.co2) + incomingStatus.no2 = pickNewestReading(currentStatus.no2, incomingStatus.no2) + incomingStatus.o2 = pickNewestReading(currentStatus.o2, incomingStatus.o2) + incomingStatus.lel = pickNewestReading(currentStatus.lel, incomingStatus.lel) + incomingStatus.h2s = pickNewestReading(currentStatus.h2s, incomingStatus.h2s) + + const currentLastActive = getTimestampMillis(currentStatus.lastActive) + const incomingLastActive = getTimestampMillis(incomingStatus.lastActive) + if ( + currentLastActive !== undefined && + incomingLastActive !== undefined && + incomingLastActive < currentLastActive + ) { + incomingStatus.lastActive = currentStatus.lastActive + } + + updated.status = incomingStatus return updated }) )