Websocket fixes.

This commit is contained in:
2026-01-11 00:46:49 -06:00
parent d4855040d8
commit 174abb7f56
32 changed files with 2770 additions and 32 deletions

View File

@ -21,7 +21,9 @@ export const TakeBetModal = ({
event,
}: TakeBetModalProps) => {
const queryClient = useQueryClient()
const betInfo = event.spread_grid[spread.toString()]
// spread_grid returns an array of bets at this spread - find the specific bet by ID
const betsAtSpread = event.spread_grid[spread.toString()] || []
const betInfo = betsAtSpread.find(b => b.bet_id === betId)
const takeBetMutation = useMutation({
mutationFn: () => spreadBetsApi.takeBet(betId),

View File

@ -0,0 +1,142 @@
import { useEffect, useRef, useCallback } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import { WS_URL } from '@/utils/constants'
import { useAuthStore } from '@/store'
export interface WebSocketMessage {
type: 'bet_created' | 'bet_taken' | 'bet_cancelled' | 'event_updated'
data: {
event_id: number
bet_id?: number
[key: string]: unknown
}
}
interface UseEventWebSocketOptions {
eventId: number
onBetCreated?: (data: WebSocketMessage['data']) => void
onBetTaken?: (data: WebSocketMessage['data']) => void
onBetCancelled?: (data: WebSocketMessage['data']) => void
}
export function useEventWebSocket({ eventId, onBetCreated, onBetTaken, onBetCancelled }: UseEventWebSocketOptions) {
const wsRef = useRef<WebSocket | null>(null)
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const queryClient = useQueryClient()
const { token } = useAuthStore()
// Use refs for callbacks to avoid reconnecting when they change
const onBetCreatedRef = useRef(onBetCreated)
const onBetTakenRef = useRef(onBetTaken)
const onBetCancelledRef = useRef(onBetCancelled)
// Update refs when callbacks change
useEffect(() => {
onBetCreatedRef.current = onBetCreated
onBetTakenRef.current = onBetTaken
onBetCancelledRef.current = onBetCancelled
}, [onBetCreated, onBetTaken, onBetCancelled])
const invalidateEventQueries = useCallback(() => {
console.log('[WebSocket] Refetching queries for event', eventId)
// Force refetch queries - use refetchQueries for immediate update
queryClient.refetchQueries({
predicate: (query) => {
const key = query.queryKey
const match = Array.isArray(key) && key[0] === 'sport-event' && key[1] === eventId
if (match) console.log('[WebSocket] Refetching query:', key)
return match
},
})
queryClient.refetchQueries({ queryKey: ['my-active-bets'] })
}, [queryClient, eventId])
useEffect(() => {
// Build WebSocket URL with token and event subscription
const wsToken = token || 'guest'
const wsUrl = `${WS_URL}/api/v1/ws?token=${wsToken}&event_id=${eventId}`
const connect = () => {
// Clean up existing connection
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
return // Already connected
}
try {
console.log(`[WebSocket] Connecting to ${wsUrl}`)
const ws = new WebSocket(wsUrl)
ws.onopen = () => {
console.log(`[WebSocket] Connected to event ${eventId}`)
}
ws.onmessage = (event) => {
try {
const message: WebSocketMessage = JSON.parse(event.data)
console.log('[WebSocket] Message received:', message)
// Only process messages for this event
if (message.data?.event_id !== eventId) return
switch (message.type) {
case 'bet_created':
onBetCreatedRef.current?.(message.data)
invalidateEventQueries()
break
case 'bet_taken':
onBetTakenRef.current?.(message.data)
invalidateEventQueries()
break
case 'bet_cancelled':
onBetCancelledRef.current?.(message.data)
invalidateEventQueries()
break
case 'event_updated':
invalidateEventQueries()
break
}
} catch (err) {
console.error('[WebSocket] Failed to parse message:', err)
}
}
ws.onclose = (event) => {
console.log(`[WebSocket] Disconnected from event ${eventId}`, event.code, event.reason)
wsRef.current = null
// Attempt to reconnect after 3 seconds (unless it was a normal closure)
if (event.code !== 1000) {
reconnectTimeoutRef.current = setTimeout(() => {
console.log('[WebSocket] Attempting to reconnect...')
connect()
}, 3000)
}
}
ws.onerror = (error) => {
console.error('[WebSocket] Error:', error)
}
wsRef.current = ws
} catch (err) {
console.error('[WebSocket] Failed to connect:', err)
}
}
connect()
return () => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current)
}
if (wsRef.current) {
wsRef.current.close(1000, 'Component unmounted')
wsRef.current = null
}
}
}, [eventId, token, invalidateEventQueries])
return {
isConnected: wsRef.current?.readyState === WebSocket.OPEN,
}
}

View File

@ -9,6 +9,8 @@ import { Button } from '@/components/common/Button'
import { Loading } from '@/components/common/Loading'
import { Header } from '@/components/layout/Header'
import { ChevronLeft } from 'lucide-react'
import { useEventWebSocket } from '@/hooks/useEventWebSocket'
import toast from 'react-hot-toast'
export const EventDetail = () => {
const { id } = useParams<{ id: string }>()
@ -25,6 +27,29 @@ export const EventDetail = () => {
enabled: eventId > 0,
})
// Connect to WebSocket for live updates
useEventWebSocket({
eventId,
onBetCreated: (data) => {
toast.success(`New bet: $${data.stake_amount} on ${data.team} at ${data.spread}`, {
icon: '📈',
duration: 3000,
})
},
onBetTaken: (data) => {
toast.success(`Bet matched: $${data.stake_amount} at ${data.spread}`, {
icon: '🤝',
duration: 3000,
})
},
onBetCancelled: (data) => {
toast(`Bet cancelled at ${data.spread}`, {
icon: '❌',
duration: 3000,
})
},
})
const handleBetCreated = () => {
// Refetch event data to show new bet
queryClient.invalidateQueries({ queryKey: ['sport-event', eventId] })
@ -42,12 +67,12 @@ export const EventDetail = () => {
<div className="min-h-screen bg-gray-50">
<Header />
<div className="px-4 sm:px-6 lg:px-8 py-8">
<Link to="/">
{/* <Link to="/">
<Button variant="secondary">
Back to Events
</Button>
</Link>
</Link> */}
<div className="mt-8">
<Loading />
</div>
@ -61,12 +86,12 @@ export const EventDetail = () => {
<div className="min-h-screen bg-gray-50">
<Header />
<div className="px-4 sm:px-6 lg:px-8 py-8">
<Link to="/">
{/* <Link to="/">
<Button variant="secondary">
Back to Events
</Button>
</Link>
</Link> */}
<div className="mt-8 text-center py-12 bg-white rounded-lg shadow">
<p className="text-gray-500">Event not found</p>
</div>
@ -79,12 +104,12 @@ export const EventDetail = () => {
<div className="min-h-screen bg-gray-50">
<Header />
<div className="px-4 sm:px-6 lg:px-8 py-8">
<Link to="/">
{/* <Link to="/">
<Button variant="secondary" className="mb-6">
Back to Events
</Button>
</Link>
</Link> */}
{/* Trading Panel - Exchange-style interface */}
<div className="mb-8">
@ -109,7 +134,7 @@ export const EventDetail = () => {
onBetTaken={handleBetTaken}
/>
</div> */}
</div>
</div>
)