useWebSocket.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. 'use client'
  2. import { useEffect, useRef, useState, useCallback } from 'react'
  3. import type { VisitorDot } from '@/types'
  4. interface WSMessage {
  5. type: string
  6. data: { visitors?: VisitorDot[] }
  7. }
  8. const INITIAL_RECONNECT_DELAY = 1000
  9. const MAX_RECONNECT_DELAY = 30000
  10. export function useWebSocket(enabled: boolean) {
  11. const [visitors, setVisitors] = useState<VisitorDot[]>([])
  12. const [connected, setConnected] = useState(false)
  13. const wsRef = useRef<WebSocket | null>(null)
  14. const reconnectTimerRef = useRef<ReturnType<typeof setTimeout>>()
  15. const reconnectAttemptRef = useRef(0)
  16. const enabledRef = useRef(enabled)
  17. const mountedRef = useRef(true)
  18. enabledRef.current = enabled
  19. const sendPosition = useCallback(() => {
  20. if (!navigator.geolocation) return
  21. navigator.geolocation.getCurrentPosition(
  22. (pos) => {
  23. const ws = wsRef.current
  24. if (ws && ws.readyState === WebSocket.OPEN) {
  25. ws.send(JSON.stringify({
  26. type: 'visitor_update',
  27. data: { lat: pos.coords.latitude, lng: pos.coords.longitude },
  28. }))
  29. }
  30. },
  31. () => {},
  32. { enableHighAccuracy: false, timeout: 5000, maximumAge: 60000 },
  33. )
  34. }, [])
  35. useEffect(() => {
  36. if (!enabled) return
  37. const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080/api/v1'
  38. const wsUrl = apiUrl.replace(/^http/, 'ws') + '/ws/visitors'
  39. function connect() {
  40. if (!enabledRef.current || !mountedRef.current) return
  41. let ws: WebSocket
  42. try {
  43. ws = new WebSocket(wsUrl)
  44. } catch {
  45. scheduleReconnect()
  46. return
  47. }
  48. wsRef.current = ws
  49. ws.onopen = () => {
  50. if (!mountedRef.current) return
  51. setConnected(true)
  52. reconnectAttemptRef.current = 0
  53. sendPosition()
  54. }
  55. ws.onmessage = (event) => {
  56. try {
  57. const msg: WSMessage = JSON.parse(event.data)
  58. if (msg.type === 'visitors' && msg.data?.visitors) {
  59. setVisitors(msg.data.visitors)
  60. }
  61. } catch { /* ignore malformed */ }
  62. }
  63. ws.onclose = () => {
  64. if (!mountedRef.current) return
  65. setConnected(false)
  66. wsRef.current = null
  67. scheduleReconnect()
  68. }
  69. ws.onerror = () => {
  70. ws.close()
  71. }
  72. }
  73. function scheduleReconnect() {
  74. if (!enabledRef.current || !mountedRef.current) return
  75. const delay = Math.min(
  76. INITIAL_RECONNECT_DELAY * Math.pow(2, reconnectAttemptRef.current),
  77. MAX_RECONNECT_DELAY,
  78. )
  79. reconnectAttemptRef.current++
  80. reconnectTimerRef.current = setTimeout(connect, delay)
  81. }
  82. connect()
  83. return () => {
  84. mountedRef.current = false
  85. clearTimeout(reconnectTimerRef.current)
  86. if (wsRef.current) {
  87. wsRef.current.onclose = null
  88. wsRef.current.close()
  89. wsRef.current = null
  90. }
  91. }
  92. }, [enabled, sendPosition])
  93. useEffect(() => {
  94. if (!connected) return
  95. const interval = setInterval(sendPosition, 60000)
  96. return () => clearInterval(interval)
  97. }, [connected, sendPosition])
  98. return { visitors, connected }
  99. }