refactor usePolygonStates to use ws topic subscription

This commit is contained in:
Josh Hawkins 2026-03-09 08:27:07 -05:00
parent d273ff85bc
commit 14c3e08ad3

View File

@ -1,18 +1,70 @@
import { useMemo } from "react";
import { useCallback, useMemo, useSyncExternalStore } from "react";
import { Polygon } from "@/types/canvas";
import { useWsState } from "@/api/ws";
import { subscribeWsTopic, getWsTopicValue } from "@/api/ws";
/**
* Hook to get enabled state for a polygon from websocket state.
* Memoizes the lookup function to avoid unnecessary re-renders.
* Subscribes to all relevant per-polygon topics so it only re-renders
* when one of those specific topics changes not on every WS update.
*/
export function usePolygonStates(polygons: Polygon[]) {
const wsState = useWsState();
// Build a stable sorted list of topics we need to watch
const topics = useMemo(() => {
const set = new Set<string>();
polygons.forEach((polygon) => {
const topic =
polygon.type === "zone"
? `${polygon.camera}/zone/${polygon.name}/state`
: polygon.type === "motion_mask"
? `${polygon.camera}/motion_mask/${polygon.name}/state`
: `${polygon.camera}/object_mask/${polygon.name}/state`;
set.add(topic);
});
return Array.from(set).sort();
}, [polygons]);
// Create a memoized lookup map that only updates when relevant ws values change
// Stable key for the topic list so subscribe/getSnapshot stay in sync
const topicsKey = topics.join("\0");
// Subscribe to all topics at once — re-subscribe only when the set changes
const subscribe = useCallback(
(listener: () => void) => {
const unsubscribes = topicsKey
.split("\0")
.filter(Boolean)
.map((topic) => subscribeWsTopic(topic, listener));
return () => unsubscribes.forEach((unsub) => unsub());
},
[topicsKey],
);
// Build a snapshot string from the current values of all topics.
// useSyncExternalStore uses Object.is, so we return a primitive that
// changes only when an observed topic's value changes.
const getSnapshot = useCallback(() => {
return topicsKey
.split("\0")
.filter(Boolean)
.map((topic) => `${topic}=${getWsTopicValue(topic) ?? ""}`)
.join("\0");
}, [topicsKey]);
const snapshot = useSyncExternalStore(subscribe, getSnapshot);
// Parse the snapshot into a lookup map
return useMemo(() => {
const stateMap = new Map<string, boolean>();
// Build value map from snapshot
const valueMap = new Map<string, unknown>();
snapshot.split("\0").forEach((entry) => {
const eqIdx = entry.indexOf("=");
if (eqIdx > 0) {
const topic = entry.slice(0, eqIdx);
const val = entry.slice(eqIdx + 1) || undefined;
valueMap.set(topic, val);
}
});
const stateMap = new Map<string, boolean>();
polygons.forEach((polygon) => {
const topic =
polygon.type === "zone"
@ -21,7 +73,7 @@ export function usePolygonStates(polygons: Polygon[]) {
? `${polygon.camera}/motion_mask/${polygon.name}/state`
: `${polygon.camera}/object_mask/${polygon.name}/state`;
const wsValue = wsState[topic];
const wsValue = valueMap.get(topic);
const enabled =
wsValue === "ON"
? true
@ -40,5 +92,5 @@ export function usePolygonStates(polygons: Polygon[]) {
true
);
};
}, [polygons, wsState]);
}, [polygons, snapshot]);
}