'use client' import { useEffect, useRef, useState, useCallback } from 'react' import type { VisitorDot } from '@/types' interface WSMessage { type: string data: { visitors?: VisitorDot[] } } const INITIAL_RECONNECT_DELAY = 1000 const MAX_RECONNECT_DELAY = 30000 export function useWebSocket(enabled: boolean) { const [visitors, setVisitors] = useState([]) const [connected, setConnected] = useState(false) const wsRef = useRef(null) const reconnectTimerRef = useRef>() const reconnectAttemptRef = useRef(0) const enabledRef = useRef(enabled) const mountedRef = useRef(true) enabledRef.current = enabled const sendPosition = useCallback(() => { if (!navigator.geolocation) return navigator.geolocation.getCurrentPosition( (pos) => { const ws = wsRef.current if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'visitor_update', data: { lat: pos.coords.latitude, lng: pos.coords.longitude }, })) } }, () => {}, { enableHighAccuracy: false, timeout: 5000, maximumAge: 60000 }, ) }, []) useEffect(() => { if (!enabled) return const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080/api/v1' const wsUrl = apiUrl.replace(/^http/, 'ws') + '/ws/visitors' function connect() { if (!enabledRef.current || !mountedRef.current) return let ws: WebSocket try { ws = new WebSocket(wsUrl) } catch { scheduleReconnect() return } wsRef.current = ws ws.onopen = () => { if (!mountedRef.current) return setConnected(true) reconnectAttemptRef.current = 0 sendPosition() } ws.onmessage = (event) => { try { const msg: WSMessage = JSON.parse(event.data) if (msg.type === 'visitors' && msg.data?.visitors) { setVisitors(msg.data.visitors) } } catch { /* ignore malformed */ } } ws.onclose = () => { if (!mountedRef.current) return setConnected(false) wsRef.current = null scheduleReconnect() } ws.onerror = () => { ws.close() } } function scheduleReconnect() { if (!enabledRef.current || !mountedRef.current) return const delay = Math.min( INITIAL_RECONNECT_DELAY * Math.pow(2, reconnectAttemptRef.current), MAX_RECONNECT_DELAY, ) reconnectAttemptRef.current++ reconnectTimerRef.current = setTimeout(connect, delay) } connect() return () => { mountedRef.current = false clearTimeout(reconnectTimerRef.current) if (wsRef.current) { wsRef.current.onclose = null wsRef.current.close() wsRef.current = null } } }, [enabled, sendPosition]) useEffect(() => { if (!connected) return const interval = setInterval(sendPosition, 60000) return () => clearInterval(interval) }, [connected, sendPosition]) return { visitors, connected } }