Websocket fixes.
This commit is contained in:
@ -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),
|
||||
|
||||
142
frontend/src/hooks/useEventWebSocket.ts
Normal file
142
frontend/src/hooks/useEventWebSocket.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user