|
1 | | -import { useCallback, useEffect, useRef, useState } from 'react'; |
2 | | -function resolveNestedOptions(options) { |
3 | | - if (options === true) return {}; |
4 | | - return options; |
5 | | -} |
| 1 | +import { useEffect, useRef, useState } from 'react'; |
| 2 | +import { getRetry } from '@/utils/helpers'; |
6 | 3 | /** |
7 | | - * Reactive wrapper for EventSource in React |
| 4 | + * @name useEventSource |
| 5 | + * @description - Hook that provides a reactive wrapper for event source |
| 6 | + * @category Browser |
8 | 7 | * |
9 | | - * @param url The URL of the EventSource |
10 | | - * @param events List of events to listen to |
11 | | - * @param options Configuration options |
| 8 | + * @browserapi EventSource https://developer.mozilla.org/en-US/docs/Web/API/EventSource |
| 9 | + * |
| 10 | + * @param {string | URL} url The URL of the EventSource |
| 11 | + * @param {string[]} [events=[]] List of events to listen to |
| 12 | + * @param {UseEventSourceOptions} [options={}] Configuration options |
| 13 | + * @returns {UseEventSourceReturn<Data>} The EventSource state and controls |
| 14 | + * |
| 15 | + * @example |
| 16 | + * const { instance, data, isConnecting, isOpen, isError, close, open } = useEventSource('url', ['message']); |
12 | 17 | */ |
13 | | -export function useEventSource(url, events = [], options = {}) { |
14 | | - const [data, setData] = useState(null); |
15 | | - const [status, setStatus] = useState('CONNECTING'); |
16 | | - const [event, setEvent] = useState(null); |
17 | | - const [error, setError] = useState(null); |
18 | | - const [lastEventId, setLastEventId] = useState(null); |
19 | | - const eventSourceRef = useRef(null); |
20 | | - const explicitlyClosedRef = useRef(false); |
21 | | - const retriedRef = useRef(0); |
22 | | - const { withCredentials = false, immediate = true, autoConnect = true, autoReconnect } = options; |
23 | | - const close = useCallback(() => { |
24 | | - if (eventSourceRef.current) { |
25 | | - eventSourceRef.current.close(); |
26 | | - eventSourceRef.current = null; |
27 | | - setStatus('CLOSED'); |
28 | | - explicitlyClosedRef.current = true; |
29 | | - } |
30 | | - }, []); |
31 | | - const open = useCallback(() => { |
32 | | - if (!url) return; |
| 18 | +export const useEventSource = (url, events = [], options = {}) => { |
| 19 | + const [isConnecting, setIsConnecting] = useState(false); |
| 20 | + const [isOpen, setIsOpen] = useState(false); |
| 21 | + const [isError, setIsError] = useState(false); |
| 22 | + const retryCountRef = useRef(options?.retry ? getRetry(options.retry) : 0); |
| 23 | + const [error, setError] = useState(undefined); |
| 24 | + const [data, setData] = useState(options?.placeholderData); |
| 25 | + const eventSourceRef = useRef(undefined); |
| 26 | + const immediately = options.immediately ?? true; |
| 27 | + const close = () => { |
| 28 | + if (!eventSourceRef.current) return; |
| 29 | + eventSourceRef.current.close(); |
| 30 | + eventSourceRef.current = undefined; |
| 31 | + setIsOpen(false); |
| 32 | + setIsConnecting(false); |
| 33 | + setIsError(false); |
| 34 | + }; |
| 35 | + const open = () => { |
33 | 36 | close(); |
34 | | - explicitlyClosedRef.current = false; |
35 | | - retriedRef.current = 0; |
36 | | - const es = new EventSource(url, { withCredentials }); |
37 | | - eventSourceRef.current = es; |
38 | | - setStatus('CONNECTING'); |
39 | | - es.onopen = () => { |
40 | | - setStatus('OPEN'); |
41 | | - setError(null); |
| 37 | + const eventSource = new EventSource(url, { withCredentials: options.withCredentials ?? false }); |
| 38 | + eventSourceRef.current = eventSource; |
| 39 | + setIsConnecting(true); |
| 40 | + eventSource.onopen = () => { |
| 41 | + setIsOpen(true); |
| 42 | + setIsConnecting(false); |
| 43 | + setError(undefined); |
| 44 | + options?.onOpen?.(); |
42 | 45 | }; |
43 | | - es.onerror = (e) => { |
44 | | - setStatus('CLOSED'); |
45 | | - setError(e); |
46 | | - // Reconnect logic |
47 | | - if (es.readyState === 2 && !explicitlyClosedRef.current && autoReconnect) { |
48 | | - es.close(); |
49 | | - const { retries = -1, delay = 1000, onFailed } = resolveNestedOptions(autoReconnect); |
50 | | - retriedRef.current += 1; |
51 | | - if (typeof retries === 'number' && (retries < 0 || retriedRef.current < retries)) { |
52 | | - setTimeout(open, delay); |
53 | | - } else if (typeof retries === 'function' && retries()) { |
54 | | - setTimeout(open, delay); |
55 | | - } else { |
56 | | - onFailed?.(); |
| 46 | + eventSource.onerror = (event) => { |
| 47 | + setIsOpen(false); |
| 48 | + setIsConnecting(false); |
| 49 | + setIsError(true); |
| 50 | + setError(event); |
| 51 | + options?.onError?.(event); |
| 52 | + if (retryCountRef.current > 0) { |
| 53 | + retryCountRef.current -= 1; |
| 54 | + const retryDelay = |
| 55 | + typeof options?.retryDelay === 'function' |
| 56 | + ? options?.retryDelay(retryCountRef.current, event) |
| 57 | + : options?.retryDelay; |
| 58 | + if (retryDelay) { |
| 59 | + setTimeout(open, retryDelay); |
| 60 | + return; |
57 | 61 | } |
58 | 62 | } |
| 63 | + retryCountRef.current = options?.retry ? getRetry(options.retry) : 0; |
59 | 64 | }; |
60 | | - es.onmessage = (e) => { |
61 | | - setEvent(null); |
62 | | - setData(e.data); |
63 | | - setLastEventId(e.lastEventId); |
| 65 | + eventSource.onmessage = (event) => { |
| 66 | + const data = options?.select ? options?.select(event.data) : event.data; |
| 67 | + setData(data); |
| 68 | + options?.onMessage?.(event); |
64 | 69 | }; |
65 | 70 | events.forEach((eventName) => { |
66 | | - es.addEventListener(eventName, (e) => { |
67 | | - setEvent(eventName); |
68 | | - setData(e.data || null); |
| 71 | + eventSource.addEventListener(eventName, (event) => { |
| 72 | + setData(event.data); |
69 | 73 | }); |
70 | 74 | }); |
71 | | - }, [url, withCredentials, autoReconnect, events, close]); |
72 | | - useEffect(() => { |
73 | | - if (immediate) open(); |
74 | | - return () => close(); |
75 | | - }, [immediate, open, close]); |
| 75 | + }; |
76 | 76 | useEffect(() => { |
77 | | - if (autoConnect) open(); |
78 | | - }, [url, autoConnect, open]); |
| 77 | + if (!immediately) return; |
| 78 | + open(); |
| 79 | + return () => { |
| 80 | + close(); |
| 81 | + }; |
| 82 | + }, [immediately]); |
79 | 83 | return { |
| 84 | + instance: eventSourceRef.current, |
80 | 85 | data, |
81 | | - status, |
82 | | - event, |
83 | 86 | error, |
| 87 | + isConnecting, |
| 88 | + isOpen, |
| 89 | + isError, |
84 | 90 | close, |
85 | | - open, |
86 | | - lastEventId |
| 91 | + open |
87 | 92 | }; |
88 | | -} |
| 93 | +}; |
0 commit comments