| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118 |
- '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<VisitorDot[]>([])
- const [connected, setConnected] = useState(false)
- const wsRef = useRef<WebSocket | null>(null)
- const reconnectTimerRef = useRef<ReturnType<typeof setTimeout>>()
- 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 }
- }
|