mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
slopfixing / issue 007
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useSessionSelector } from '../context/SessionContext.jsx';
|
||||
import { useSharedClock } from './useSharedClock.js';
|
||||
|
||||
export function useDriverVideoModePolicy(roverId) {
|
||||
const mode = useSessionSelector((state) => state.session?.mode || null);
|
||||
@@ -8,7 +9,13 @@ export function useDriverVideoModePolicy(roverId) {
|
||||
const turnQueues = useSessionSelector((state) => state.session?.turnQueues ?? {});
|
||||
const socketId = useSessionSelector((state) => state.session?.socketId || null);
|
||||
const activeDrivers = useSessionSelector((state) => state.session?.activeDrivers ?? {});
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
const isTurnsMode = mode === 'turns';
|
||||
/*
|
||||
This policy only switches preview/full video around a multi-second turn
|
||||
boundary. A shared one-second clock is responsive enough for that UI policy
|
||||
and avoids running a separate 250ms timer beside the TurnsOverlay countdown.
|
||||
*/
|
||||
const now = useSharedClock(1000, isTurnsMode);
|
||||
|
||||
const turnInfo = roverId ? turnQueues?.[roverId] || null : null;
|
||||
const activeDriverId = roverId ? activeDrivers?.[roverId] || null : null;
|
||||
@@ -19,11 +26,10 @@ export function useDriverVideoModePolicy(roverId) {
|
||||
const idx = queue.findIndex((id) => id === turnInfo.current);
|
||||
if (idx === -1) return queue[0] || null;
|
||||
return queue[(idx + 1) % queue.length] || null;
|
||||
}, [turnInfo?.queue, turnInfo?.current]);
|
||||
}, [turnInfo]);
|
||||
const isNextDriver = Boolean(socketId && nextDriverId === socketId);
|
||||
const deadline = turnInfo?.deadline || null;
|
||||
const msUntilTurn = deadline ? deadline - now : null;
|
||||
const isTurnsMode = mode === 'turns';
|
||||
const totalRovers = roster.length;
|
||||
const totalDrivers = useMemo(() => {
|
||||
const unique = new Set();
|
||||
@@ -43,11 +49,5 @@ export function useDriverVideoModePolicy(roverId) {
|
||||
const showNotTurnNotice = isTurnsMode && !isActiveDriver;
|
||||
const forceSnapshotByTurnPolicy = showNotTurnNotice && !isPreSwitchWindow && shouldUsePreviewByLoad;
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== 'turns') return undefined;
|
||||
const timer = setInterval(() => setNow(Date.now()), 250);
|
||||
return () => clearInterval(timer);
|
||||
}, [mode]);
|
||||
|
||||
return forceSnapshotByTurnPolicy ? 'snapshot' : null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// Shared Clock Hook
|
||||
// Purpose: Lets display-only countdown components share one browser interval per cadence.
|
||||
// Scope: Keeps timers out of control/safety paths; callers opt in only for UI text that can tolerate coarse updates.
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const clockStores = new Map();
|
||||
|
||||
function nowMs() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function normalizeDelay(delayMs) {
|
||||
const delay = Number(delayMs);
|
||||
if (!Number.isFinite(delay) || delay <= 0) return 1000;
|
||||
return Math.max(16, Math.round(delay));
|
||||
}
|
||||
|
||||
function getStore(delayMs) {
|
||||
const delay = normalizeDelay(delayMs);
|
||||
const existing = clockStores.get(delay);
|
||||
if (existing) return existing;
|
||||
|
||||
const store = {
|
||||
delay,
|
||||
timer: null,
|
||||
visibilityHandler: null,
|
||||
current: nowMs(),
|
||||
listeners: new Set(),
|
||||
};
|
||||
clockStores.set(delay, store);
|
||||
return store;
|
||||
}
|
||||
|
||||
function stopStore(store) {
|
||||
if (store.timer) {
|
||||
clearInterval(store.timer);
|
||||
store.timer = null;
|
||||
}
|
||||
if (store.visibilityHandler && typeof document !== 'undefined') {
|
||||
document.removeEventListener('visibilitychange', store.visibilityHandler);
|
||||
store.visibilityHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
function publish(store) {
|
||||
store.current = nowMs();
|
||||
store.listeners.forEach((listener) => listener(store.current));
|
||||
}
|
||||
|
||||
function ensureVisibilityHandler(store) {
|
||||
if (store.visibilityHandler || typeof document === 'undefined') return;
|
||||
store.visibilityHandler = () => {
|
||||
if (!store.listeners.size) return;
|
||||
if (document.visibilityState === 'visible') {
|
||||
/*
|
||||
Hidden tabs do not need display-only countdown work. When the tab comes
|
||||
back, publish immediately so labels catch up before the next interval.
|
||||
*/
|
||||
publish(store);
|
||||
startStore(store);
|
||||
return;
|
||||
}
|
||||
if (store.timer) {
|
||||
clearInterval(store.timer);
|
||||
store.timer = null;
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', store.visibilityHandler);
|
||||
}
|
||||
|
||||
function startStore(store) {
|
||||
if (store.timer || !store.listeners.size) return;
|
||||
ensureVisibilityHandler(store);
|
||||
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return;
|
||||
/*
|
||||
One interval fans out to every mounted consumer using the same cadence. The
|
||||
previous pattern created independent intervals in each countdown component,
|
||||
which meant several panels could wake the main thread separately even though
|
||||
they only needed the same "current time" value for human-readable labels.
|
||||
*/
|
||||
store.timer = setInterval(() => {
|
||||
publish(store);
|
||||
}, store.delay);
|
||||
}
|
||||
|
||||
function subscribe(store, listener) {
|
||||
store.listeners.add(listener);
|
||||
startStore(store);
|
||||
|
||||
return () => {
|
||||
store.listeners.delete(listener);
|
||||
/*
|
||||
Stop the interval as soon as the last consumer leaves. This matters for
|
||||
tab panels and overlays because hidden or unmounted UI should not keep a
|
||||
display clock alive just because another render path created it earlier.
|
||||
*/
|
||||
if (!store.listeners.size) {
|
||||
stopStore(store);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function useSharedClock(delayMs = 1000, enabled = true) {
|
||||
const [now, setNow] = useState(() => nowMs());
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return undefined;
|
||||
const store = getStore(delayMs);
|
||||
/*
|
||||
Subscribing starts the shared interval but does not synchronously push
|
||||
state from inside the effect. React's newer lint rules flag synchronous
|
||||
effect-time state writes because they can cascade renders; the hook's
|
||||
initial Date.now state is already fresh for the first paint, and later
|
||||
updates arrive through the interval or visibility callback.
|
||||
*/
|
||||
return subscribe(store, setNow);
|
||||
}, [delayMs, enabled]);
|
||||
|
||||
return now;
|
||||
}
|
||||
Reference in New Issue
Block a user