Add WS reconnect logic + connection indicator in calendar
- useHotelSocket: reconnect with exponential backoff (up to 30s), return connected state, log errors to console - BookingCalendar: show green/gray dot + Online/Offline label in toolbar - CalendarPage: pass wsConnected prop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useCallback } from 'react'
|
||||
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||
import type { Booking } from '../types'
|
||||
|
||||
export type WsMessage =
|
||||
@@ -15,30 +15,74 @@ interface Options {
|
||||
}
|
||||
|
||||
export function useHotelSocket({ slug, token, onMessage }: Options) {
|
||||
const wsRef = useRef<WebSocket | null>(null)
|
||||
const cbRef = useRef(onMessage)
|
||||
cbRef.current = onMessage
|
||||
const wsRef = useRef<WebSocket | null>(null)
|
||||
const cbRef = useRef(onMessage)
|
||||
cbRef.current = onMessage
|
||||
|
||||
const [connected, setConnected] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!slug || !token) return
|
||||
|
||||
const base = (import.meta.env.VITE_API_URL as string | undefined) ?? 'https://api.hotelsync.ru'
|
||||
const wsBase = base.replace(/^https/, 'wss').replace(/^http(?!s)/, 'ws')
|
||||
const ws = new WebSocket(`${wsBase}/ws?hotel=${encodeURIComponent(slug)}&token=${encodeURIComponent(token)}`)
|
||||
wsRef.current = ws
|
||||
let destroyed = false
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let attempts = 0
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
try { cbRef.current(JSON.parse(e.data as string) as WsMessage) } catch { /* ignore */ }
|
||||
function connect() {
|
||||
if (destroyed) return
|
||||
|
||||
const base = (import.meta.env.VITE_API_URL as string | undefined) ?? 'https://api.hotelsync.ru'
|
||||
const wsBase = base.replace(/^https/, 'wss').replace(/^http(?!s)/, 'ws')
|
||||
const url = `${wsBase}/ws?hotel=${encodeURIComponent(slug)}&token=${encodeURIComponent(token!)}`
|
||||
|
||||
const ws = new WebSocket(url)
|
||||
wsRef.current = ws
|
||||
|
||||
ws.onopen = () => {
|
||||
if (destroyed) { ws.close(); return }
|
||||
setConnected(true)
|
||||
attempts = 0
|
||||
console.debug('[WS] connected', slug)
|
||||
}
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
try { cbRef.current(JSON.parse(e.data as string) as WsMessage) } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
ws.onerror = (ev) => {
|
||||
console.warn('[WS] error', ev)
|
||||
}
|
||||
|
||||
ws.onclose = (ev) => {
|
||||
wsRef.current = null
|
||||
setConnected(false)
|
||||
if (destroyed) return
|
||||
const delay = Math.min(1000 * 2 ** attempts, 30_000)
|
||||
attempts++
|
||||
console.debug(`[WS] closed (${ev.code}), reconnecting in ${delay}ms (attempt ${attempts})`)
|
||||
reconnectTimer = setTimeout(connect, delay)
|
||||
}
|
||||
}
|
||||
ws.onerror = () => { /* silent */ }
|
||||
|
||||
return () => { ws.close(); wsRef.current = null }
|
||||
connect()
|
||||
|
||||
return () => {
|
||||
destroyed = true
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer)
|
||||
wsRef.current?.close()
|
||||
wsRef.current = null
|
||||
setConnected(false)
|
||||
}
|
||||
}, [slug, token])
|
||||
|
||||
const send = useCallback((msg: WsMessage) => {
|
||||
const ws = wsRef.current
|
||||
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg))
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(msg))
|
||||
} else {
|
||||
console.warn('[WS] send skipped — not connected', msg)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { send }
|
||||
return { send, connected }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user