73 lines
2.2 KiB
TypeScript
73 lines
2.2 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
import type { ClientMessage, GameView, Session } from './types'
|
|
|
|
interface ServerMessage {
|
|
type: 'state' | 'error'
|
|
state?: GameView
|
|
error?: string
|
|
}
|
|
|
|
// useGame owns the WebSocket for a session: it keeps the latest server view,
|
|
// reconnects with backoff, and exposes send() for actions. Server-rejected
|
|
// actions surface as a transient `error`.
|
|
export function useGame(session: Session) {
|
|
const [view, setView] = useState<GameView | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [connected, setConnected] = useState(false)
|
|
const wsRef = useRef<WebSocket | null>(null)
|
|
const errorTimer = useRef<number>(undefined)
|
|
|
|
useEffect(() => {
|
|
let ws: WebSocket
|
|
let closed = false
|
|
let retryDelay = 500
|
|
let retryTimer: number | undefined
|
|
|
|
function connect() {
|
|
const proto = location.protocol === 'https:' ? 'wss' : 'ws'
|
|
const params = new URLSearchParams({
|
|
game: session.gameId,
|
|
player: session.playerId,
|
|
token: session.token,
|
|
})
|
|
ws = new WebSocket(`${proto}://${location.host}/api/ws?${params}`)
|
|
wsRef.current = ws
|
|
ws.onopen = () => {
|
|
retryDelay = 500
|
|
setConnected(true)
|
|
}
|
|
ws.onmessage = (ev) => {
|
|
const msg = JSON.parse(ev.data) as ServerMessage
|
|
if (msg.type === 'state' && msg.state) setView(msg.state)
|
|
if (msg.type === 'error' && msg.error) showError(msg.error)
|
|
}
|
|
ws.onclose = () => {
|
|
setConnected(false)
|
|
if (closed) return
|
|
retryTimer = window.setTimeout(connect, retryDelay)
|
|
retryDelay = Math.min(retryDelay * 2, 8000)
|
|
}
|
|
}
|
|
|
|
function showError(msg: string) {
|
|
setError(msg)
|
|
window.clearTimeout(errorTimer.current)
|
|
errorTimer.current = window.setTimeout(() => setError(null), 3500)
|
|
}
|
|
|
|
connect()
|
|
return () => {
|
|
closed = true
|
|
window.clearTimeout(retryTimer)
|
|
window.clearTimeout(errorTimer.current)
|
|
ws.close()
|
|
}
|
|
}, [session.gameId, session.playerId, session.token])
|
|
|
|
const send = useCallback((msg: ClientMessage) => {
|
|
wsRef.current?.send(JSON.stringify(msg))
|
|
}, [])
|
|
|
|
return { view, error, connected, send }
|
|
}
|