From 19bd4bd96c2dc529fec213568e83102e8f747353 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Tue, 23 Jun 2026 18:56:30 -0500 Subject: [PATCH] fix(player): useSonare destructure shadowed Number; dashboard metrics deps non-reactive useSonare destructured the length option as `number`, shadowing the global Number constructor and binding to undefined. The state initializer and regenerate callback both called generateSonare(number, suffixLength), producing strings like 'undefined-undefined-undefined-abc'. The result is used as a React key in player.tsx and a session id in player.tsx, so this affected actual behavior. Rename the param to length. DashboardMetricsProvider's bitrate, FPS, and segment-timing effects had store.getState().segment?.size (and similar) in their dep arrays. getState() is not reactive, so the effects ran once on mount and never updated. The bitrate and FPS refs were stuck at their initial values for the lifetime of the provider. Subscribe to the slice via useStore and depend on the reactive segment value instead. Also drop the let [sona, resetSona] = useSonare() in PlayerBackend (should be const) and remove the now-unused latestInputsRef and useShallow import from the metrics provider. --- .../dashboard/dashboard-metrics.tsx | 49 ++++++------------- js/web/src/lib/useSonare.ts | 12 ++--- 2 files changed, 21 insertions(+), 40 deletions(-) diff --git a/js/web/src/components/dashboard/dashboard-metrics.tsx b/js/web/src/components/dashboard/dashboard-metrics.tsx index 9358bb1dc..90ccb2acb 100644 --- a/js/web/src/components/dashboard/dashboard-metrics.tsx +++ b/js/web/src/components/dashboard/dashboard-metrics.tsx @@ -8,7 +8,6 @@ import { type ReactNode, } from "react"; import { useStore } from "zustand"; -import { useShallow } from "zustand/react/shallow"; import { useDashboardStore } from "./dashboard-store-context"; /** How often we sample the live metrics into the history buffer. */ @@ -97,57 +96,39 @@ export function DashboardMetricsProvider({ children }: ProviderProps) { const currentBitrateRef = useRef(0); const currentSegmentTimingRef = useRef(0); const currentFpsRef = useRef(0); - const latestInputsRef = useRef({ chat: [] as any[], viewers: 0 }); - latestInputsRef.current = { - chat: store.getState().chat, - viewers: store.getState().viewers ?? 0, - }; - - // Subscribe to the store so the refs stay current. We don't render off - // of these — they're read inside the sampling interval. - const storeState = useStore( - store, - useShallow((s) => ({ - segment: s.segment, - livestream: s.livestream, - chat: s.chat, - viewers: s.viewers, - })), - ); + + // Subscribe to the slice so the effect deps below fire when segments + // change. The effects themselves only write to refs, so we don't render + // off the segment subscription. `livestream` is read for `hasLivestream`. + const segment = useStore(store, (s) => s.segment); + const livestream = useStore(store, (s) => s.livestream); // Update bitrate from the current segment. useEffect(() => { - const seg = store.getState().segment; - if (!seg?.size || !seg?.duration) return; - const kbps = (seg.size * 8) / (seg.duration / 1_000_000_000) / 1000; + if (!segment?.size || !segment?.duration) return; + const kbps = (segment.size * 8) / (segment.duration / 1_000_000_000) / 1000; currentBitrateRef.current = kbps; - }, [ - store, - store.getState().segment?.size, - store.getState().segment?.duration, - ]); + }, [segment?.size, segment?.duration]); // Update FPS from the current segment's video track. useEffect(() => { - const seg = store.getState().segment; - const videoTrack = seg?.video?.[0]; + const videoTrack = segment?.video?.[0]; if (!videoTrack?.framerate) return; const { num, den } = videoTrack.framerate; if (!den) return; currentFpsRef.current = num / den; - }, [store, store.getState().segment?.video]); + }, [segment?.video]); // Update segment timing (ms since previous segment). const lastSegmentAtRef = useRef(null); useEffect(() => { - const seg = store.getState().segment; - if (!seg) return; + if (!segment) return; const now = Date.now(); if (lastSegmentAtRef.current !== null) { currentSegmentTimingRef.current = now - lastSegmentAtRef.current; } lastSegmentAtRef.current = now; - }, [store, store.getState().segment]); + }, [segment]); // When the user changes the range, resize the history buffer. The window // then refills as new samples arrive. @@ -195,9 +176,9 @@ export function DashboardMetricsProvider({ children }: ProviderProps) { history, range, setRange, - hasLivestream: !!storeState.livestream, + hasLivestream: !!livestream, }), - [history, range, storeState.livestream], + [history, range, livestream], ); return ( diff --git a/js/web/src/lib/useSonare.ts b/js/web/src/lib/useSonare.ts index 0b833384e..fce4cbda8 100644 --- a/js/web/src/lib/useSonare.ts +++ b/js/web/src/lib/useSonare.ts @@ -17,18 +17,18 @@ function generateSonare(length: number, suffixLength: number) { return word; } -// Sonare-based random ID generator. It generates three words, -// separated by dashes, and optionally adds a random suffix of specified length. +// Sonare-based random ID generator. It generates a string of N words +// separated by dashes, optionally with a random suffix of specified length. // Note that THIS IS NOT A CRYPTOGRAPHICALLY SECURE ID GENERATOR. export function useSonare( - { length: number, suffixLength } = { length: 2, suffixLength: 4 }, + { length, suffixLength } = { length: 2, suffixLength: 4 }, ) { - let [sona, setSona] = useState(() => { - return generateSonare(number, suffixLength); + const [sona, setSona] = useState(() => { + return generateSonare(length, suffixLength); }); const regenerate = () => { - setSona(generateSonare(number, suffixLength)); + setSona(generateSonare(length, suffixLength)); }; return [sona, regenerate] as const; -- 2.51.2