Initial commit.

This commit is contained in:
Greyson Parrelli
2026-07-22 23:07:29 -04:00
commit 612a4e6227
38 changed files with 6106 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
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 }
}