Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | 1x 1x 1x 53x 53x 53x 53x 53x 53x 53x 53x 53x 53x 53x 53x 53x 9x 9x 53x 42x 42x 42x 53x 21x 21x 21x 21x 21x 21x 11x 11x 11x 11x 11x 21x 1x 1x 1x 21x 1x 1x 1x 21x 10x 10x 10x 10x 1x 1x 1x 9x 9x 9x 9x 53x 11x 11x 11x 11x 53x | import { useEffect, useRef, useState, useCallback } from 'react'
const MAX_BACKOFF = 30
const INITIAL_BACKOFF = 1
const MAX_FAILURES = 5
export interface SSEOptions {
url: string
onEvent: (event: { type: 'created' | 'updated'; data: unknown }) => void
onConnectionLost?: () => void
enabled?: boolean
}
export interface SSEState {
status: 'connecting' | 'connected' | 'disconnected' | 'connection-lost'
reconnect: () => void
consecutiveFailures: number
}
export function useSSE(options: SSEOptions): SSEState {
const { url, onEvent, onConnectionLost, enabled = true } = options
const [status, setStatus] = useState<SSEState['status']>('disconnected')
const [consecutiveFailures, setConsecutiveFailures] = useState(0)
const esRef = useRef<EventSource | null>(null)
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const backoffRef = useRef(INITIAL_BACKOFF)
const failuresRef = useRef(0)
const onEventRef = useRef(onEvent)
const onConnectionLostRef = useRef(onConnectionLost)
onEventRef.current = onEvent
onConnectionLostRef.current = onConnectionLost
const clearTimer = useCallback(() => {
if (timerRef.current !== null) {
clearTimeout(timerRef.current)
timerRef.current = null
}
}, [])
const cleanup = useCallback(() => {
clearTimer()
esRef.current?.close()
esRef.current = null
}, [clearTimer])
const connect = useCallback(() => {
Iif (!enabled) return
cleanup()
const es = new EventSource(url)
esRef.current = es
setStatus('connecting')
es.onopen = () => {
setStatus('connected')
failuresRef.current = 0
setConsecutiveFailures(0)
backoffRef.current = INITIAL_BACKOFF
clearTimer()
}
es.addEventListener('created', (event: Event) => {
const msg = event as MessageEvent
try {
onEventRef.current({ type: 'created', data: JSON.parse(msg.data) })
} catch { /* ignore malformed */ }
})
es.addEventListener('updated', (event: Event) => {
const msg = event as MessageEvent
try {
onEventRef.current({ type: 'updated', data: JSON.parse(msg.data) })
} catch { /* ignore malformed */ }
})
es.onerror = () => {
cleanup()
failuresRef.current += 1
setConsecutiveFailures(failuresRef.current)
if (failuresRef.current >= MAX_FAILURES) {
setStatus('connection-lost')
onConnectionLostRef.current?.()
return
}
const delay = backoffRef.current * 1000
backoffRef.current = Math.min(backoffRef.current * 2, MAX_BACKOFF)
timerRef.current = setTimeout(() => {
connect()
}, delay)
}
}, [url, enabled, cleanup, clearTimer])
useEffect(() => {
Eif (enabled) connect()
return () => {
cleanup()
setStatus('disconnected')
}
}, [connect, enabled, cleanup])
return {
status,
reconnect: connect,
consecutiveFailures,
}
}
|