From 900752b6e28fd6ef1fde4650ebc4cadf03c15b00 Mon Sep 17 00:00:00 2001 From: Yevhen Shevchenko <90138953+yeshev@users.noreply.github.com> Date: Wed, 9 Aug 2023 19:45:32 +0300 Subject: [PATCH] feat: add event source provider with hook (#3298) * feat: add event source provider with hook * chore: jwt is updated --------- Co-authored-by: Palash Gupta --- .../src/hooks/useEventSourceEvent/index.ts | 22 ++++ frontend/src/providers/EventSource.tsx | 124 ++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 frontend/src/hooks/useEventSourceEvent/index.ts create mode 100644 frontend/src/providers/EventSource.tsx diff --git a/frontend/src/hooks/useEventSourceEvent/index.ts b/frontend/src/hooks/useEventSourceEvent/index.ts new file mode 100644 index 0000000000..2194d6255e --- /dev/null +++ b/frontend/src/hooks/useEventSourceEvent/index.ts @@ -0,0 +1,22 @@ +import { EventListener, EventSourceEventMap } from 'event-source-polyfill'; +import { useEventSource } from 'providers/EventSource'; +import { useEffect } from 'react'; + +export const useEventSourceEvent = ( + eventName: keyof EventSourceEventMap, + listener: EventListener, +): void => { + const { eventSourceInstance } = useEventSource(); + + useEffect(() => { + if (eventSourceInstance) { + eventSourceInstance.addEventListener(eventName, listener); + } + + return (): void => { + if (eventSourceInstance) { + eventSourceInstance.removeEventListener(eventName, listener); + } + }; + }, [eventName, eventSourceInstance, listener]); +}; diff --git a/frontend/src/providers/EventSource.tsx b/frontend/src/providers/EventSource.tsx new file mode 100644 index 0000000000..972438db77 --- /dev/null +++ b/frontend/src/providers/EventSource.tsx @@ -0,0 +1,124 @@ +import { apiV3 } from 'api/apiV1'; +import { ENVIRONMENT } from 'constants/env'; +import { EventListener, EventSourcePolyfill } from 'event-source-polyfill'; +import { + createContext, + PropsWithChildren, + useCallback, + useContext, + useMemo, + useRef, + useState, +} from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import AppReducer from 'types/reducer/app'; + +interface IEventSourceContext { + eventSourceInstance: EventSourcePolyfill | null; + isConnectionOpen: boolean; + isConnectionLoading: boolean; + isConnectionError: string; + handleStartOpenConnection: (url?: string) => void; + handleCloseConnection: () => void; +} + +const EventSourceContext = createContext({ + eventSourceInstance: null, + isConnectionOpen: false, + isConnectionLoading: false, + isConnectionError: '', + handleStartOpenConnection: () => {}, + handleCloseConnection: () => {}, +}); + +export function EventSourceProvider({ + children, +}: PropsWithChildren): JSX.Element { + const [isConnectionOpen, setIsConnectionOpen] = useState(false); + const [isConnectionLoading, setIsConnectionLoading] = useState(false); + const [isConnectionError, setIsConnectionError] = useState(''); + + const { user } = useSelector((state) => state.app); + + const eventSourceRef = useRef(null); + + const handleCloseConnection = useCallback(() => { + if (!eventSourceRef.current) return; + + eventSourceRef.current.close(); + setIsConnectionOpen(false); + setIsConnectionLoading(false); + }, []); + + const handleOpenConnection: EventListener = useCallback(() => { + setIsConnectionLoading(false); + setIsConnectionOpen(true); + }, []); + + const handleErrorConnection: EventListener = useCallback(() => { + if (!eventSourceRef.current) return; + + handleCloseConnection(); + + eventSourceRef.current.removeEventListener('error', handleErrorConnection); + eventSourceRef.current.removeEventListener('open', handleOpenConnection); + }, [handleCloseConnection, handleOpenConnection]); + + const handleStartOpenConnection = useCallback( + (url?: string) => { + const eventSourceUrl = url || `${ENVIRONMENT.baseURL}${apiV3}logs/livetail`; + + const TIMEOUT_IN_MS = 10 * 60 * 1000; + + eventSourceRef.current = new EventSourcePolyfill(eventSourceUrl, { + headers: { + Authorization: `Bearer ${user?.accessJwt}`, + }, + heartbeatTimeout: TIMEOUT_IN_MS, + }); + + setIsConnectionLoading(true); + setIsConnectionError(''); + + eventSourceRef.current.addEventListener('error', handleErrorConnection); + + eventSourceRef.current.addEventListener('open', handleOpenConnection); + }, + [handleErrorConnection, handleOpenConnection, user?.accessJwt], + ); + + const contextValue = useMemo( + () => ({ + eventSourceInstance: eventSourceRef.current, + isConnectionError, + isConnectionLoading, + isConnectionOpen, + handleStartOpenConnection, + handleCloseConnection, + }), + [ + isConnectionError, + isConnectionLoading, + isConnectionOpen, + handleStartOpenConnection, + handleCloseConnection, + ], + ); + + return ( + + {children} + + ); +} + +export const useEventSource = (): IEventSourceContext => { + const context = useContext(EventSourceContext); + + if (!context) { + throw new Error('Should be used inside the context'); + } + + return context; +};