Skip to content

Commit 75bc6ef

Browse files
atulmguptaCopilot
andcommitted
fix: SSE connect/disconnect loop β€” stabilize with callback refs
Root cause: useRealtimeEvents hook had callback dependencies in its useCallback/useEffect chain. Every parent re-render created new callback refs β†’ connect() recreated β†’ useEffect tore down old SSE β†’ new SSE opened β†’ instant connect/disconnect loop generating thousands of requests. Fix: Store callbacks in a mutable ref (cbRefs) so the connect function has zero dependencies and the SSE connection stays stable across renders. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 59c89f1 commit 75bc6ef

1 file changed

Lines changed: 17 additions & 22 deletions

File tree

β€Žweb/src/hooks/useRealtimeEvents.tsβ€Ž

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,13 @@ export function useRealtimeEvents(options: SSEOptions = {}) {
2929
const [reconnectCount, setReconnectCount] = useState(0)
3030
const sourceRef = useRef<EventSource | null>(null)
3131
const reconnectTimer = useRef<number>(undefined)
32-
const backoffRef = useRef(1000) // start at 1s, max 30s
32+
const backoffRef = useRef(1000)
3333
const eventBufferRef = useRef<BufferedEvent[]>([])
3434
const hasConnectedOnce = useRef(false)
3535

36-
const flushEventBuffer = useCallback(() => {
37-
const buffer = eventBufferRef.current
38-
eventBufferRef.current = []
39-
for (const event of buffer) {
40-
if (event.type === 'vehicle_update') onVehicleUpdate?.(event.data)
41-
else if (event.type === 'alert') onAlert?.(event.data)
42-
}
43-
}, [onVehicleUpdate, onAlert])
36+
// Store callbacks in refs to avoid recreating the connection on every render
37+
const cbRefs = useRef({ onVehicleUpdate, onAlert, onConnected, onDisconnected })
38+
cbRefs.current = { onVehicleUpdate, onAlert, onConnected, onDisconnected }
4439

4540
const connect = useCallback(() => {
4641
if (sourceRef.current) {
@@ -53,25 +48,28 @@ export function useRealtimeEvents(options: SSEOptions = {}) {
5348
source.addEventListener('connected', (e) => {
5449
setConnected(true)
5550
setStatus('connected')
56-
backoffRef.current = 1000 // reset backoff on successful connection
51+
backoffRef.current = 1000
5752
if (hasConnectedOnce.current) {
5853
setReconnectCount(c => c + 1)
5954
}
6055
hasConnectedOnce.current = true
6156
const data = JSON.parse(e.data)
62-
onConnected?.(data.client_id)
63-
// Flush any events buffered during disconnect
64-
flushEventBuffer()
57+
cbRefs.current.onConnected?.(data.client_id)
58+
// Flush buffered events
59+
const buffer = eventBufferRef.current
60+
eventBufferRef.current = []
61+
for (const evt of buffer) {
62+
if (evt.type === 'vehicle_update') cbRefs.current.onVehicleUpdate?.(evt.data)
63+
else if (evt.type === 'alert') cbRefs.current.onAlert?.(evt.data)
64+
}
6565
})
6666

6767
source.addEventListener('vehicle_update', (e) => {
68-
const data = JSON.parse(e.data)
69-
onVehicleUpdate?.(data)
68+
cbRefs.current.onVehicleUpdate?.(JSON.parse(e.data))
7069
})
7170

7271
source.addEventListener('alert', (e) => {
73-
const data = JSON.parse(e.data)
74-
onAlert?.(data)
72+
cbRefs.current.onAlert?.(JSON.parse(e.data))
7573
})
7674

7775
source.addEventListener('heartbeat', () => {
@@ -82,21 +80,18 @@ export function useRealtimeEvents(options: SSEOptions = {}) {
8280
setConnected(false)
8381
const wasConnected = hasConnectedOnce.current
8482
setStatus(wasConnected ? 'reconnecting' : 'disconnected')
85-
onDisconnected?.()
83+
cbRefs.current.onDisconnected?.()
8684
source.close()
8785
sourceRef.current = null
88-
// Exponential backoff with jitter, capped at 30s
8986
const jitter = Math.random() * 500
9087
const delay = Math.min(backoffRef.current + jitter, 30_000)
9188
backoffRef.current = Math.min(backoffRef.current * 2, 30_000)
9289
reconnectTimer.current = window.setTimeout(connect, delay)
9390
}
94-
}, [onVehicleUpdate, onAlert, onConnected, onDisconnected, flushEventBuffer])
91+
}, []) // no deps β€” callbacks accessed via stable refs
9592

96-
/** Buffer an event to replay after reconnection */
9793
const bufferEvent = useCallback((type: string, data: unknown) => {
9894
eventBufferRef.current.push({ type, data, timestamp: Date.now() })
99-
// Keep buffer bounded to prevent memory leaks
10095
if (eventBufferRef.current.length > 200) {
10196
eventBufferRef.current = eventBufferRef.current.slice(-100)
10297
}

0 commit comments

Comments
Β (0)