import { useCallback, useEffect, useRef, useState, ReactNode } from "react"; import { Button } from "@/components/ui/button"; type Peer = { id: string; openedAt: number; lastSeen: number }; const CHANNEL = "lelab-tabs-v1"; const HEARTBEAT_MS = 1000; const PEER_TIMEOUT_MS = 3000; const SingleTabGuard = ({ children }: { children: ReactNode }) => { const [isPrimary, setIsPrimary] = useState(true); const peersRef = useRef>(new Map()); const myIdRef = useRef(""); const myOpenedAtRef = useRef(0); const channelRef = useRef(null); const recompute = useCallback(() => { const peers = peersRef.current; const cutoff = Date.now() - PEER_TIMEOUT_MS; for (const [id, peer] of peers) { if (peer.lastSeen < cutoff) peers.delete(id); } let winnerId = myIdRef.current; let winnerOpenedAt = myOpenedAtRef.current; for (const peer of peers.values()) { if ( peer.openedAt < winnerOpenedAt || (peer.openedAt === winnerOpenedAt && peer.id < winnerId) ) { winnerId = peer.id; winnerOpenedAt = peer.openedAt; } } setIsPrimary(winnerId === myIdRef.current); }, []); useEffect(() => { if (typeof window === "undefined" || typeof BroadcastChannel === "undefined") { return; } myIdRef.current = crypto.randomUUID(); myOpenedAtRef.current = Date.now(); const channel = new BroadcastChannel(CHANNEL); channelRef.current = channel; const send = (type: string) => { channel.postMessage({ type, id: myIdRef.current, openedAt: myOpenedAtRef.current, }); }; channel.onmessage = (e) => { const msg = e.data; if (!msg || msg.id === myIdRef.current) return; const peers = peersRef.current; if (msg.type === "HEARTBEAT") { peers.set(msg.id, { id: msg.id, openedAt: msg.openedAt, lastSeen: Date.now(), }); } else if (msg.type === "RELEASE") { peers.delete(msg.id); } else if (msg.type === "TAKEOVER") { peers.set(msg.id, { id: msg.id, openedAt: msg.openedAt, lastSeen: Date.now(), }); // Move ourselves behind the taker so the election flips. if (myOpenedAtRef.current <= msg.openedAt) { myOpenedAtRef.current = msg.openedAt + 1; } } recompute(); }; send("HEARTBEAT"); const interval = setInterval(() => { send("HEARTBEAT"); recompute(); }, HEARTBEAT_MS); const onUnload = () => send("RELEASE"); window.addEventListener("beforeunload", onUnload); return () => { window.removeEventListener("beforeunload", onUnload); clearInterval(interval); send("RELEASE"); channel.close(); channelRef.current = null; }; }, [recompute]); const takeOver = useCallback(() => { myOpenedAtRef.current = 0; channelRef.current?.postMessage({ type: "TAKEOVER", id: myIdRef.current, openedAt: 0, }); recompute(); }, [recompute]); return ( <> {children} {!isPrimary && (

LeLab is already open in another tab

Only one tab can control the robot at a time. Switch back to the original tab, or take over here — the other tab will lock.

)} ); }; export default SingleTabGuard;