react-eventsource-hook
Version:
React hook for the EventSource interface (Server-Sent Events) with retry backoff, pause on hidden, and reconnect/close controls.
1 lines • 8.22 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/use-eventsource.ts"],"sourcesContent":["export * from \"./use-eventsource\";","import { useState, useEffect, useRef, useCallback } from \"react\";\nimport { fetchEventSource, type EventSourceMessage } from \"@microsoft/fetch-event-source\";\n\n// Options mirror native EventSource plus Fetch flexibility\nexport interface UseEventSourceOptions {\n url: string;\n withCredentials?: boolean;\n init?: RequestInit;\n fetch?: typeof window.fetch;\n retryIntervalMs?: number;\n maxRetryIntervalMs?: number;\n pauseOnHidden?: boolean;\n\n onopen?: (response: Response) => void;\n onmessage?: (message: EventSourceMessage) => void;\n onerror?: (error: unknown) => void;\n onclose?: () => void;\n}\n\n// Return values mirror EventSource state and give imperative controls\nexport interface UseEventSourceResult {\n readyState: 0 | 1 | 2;\n lastEventId: string;\n error?: any;\n events: EventSourceMessage[];\n close: () => void;\n reconnect: () => void;\n}\n\nexport function useEventSource(\n options: UseEventSourceOptions\n): UseEventSourceResult {\n const {\n url,\n withCredentials = false,\n init,\n fetch: customFetch,\n retryIntervalMs,\n maxRetryIntervalMs,\n pauseOnHidden = true,\n onopen: onOpen,\n onmessage: onMessage,\n onerror: onError,\n onclose: onClose,\n } = options;\n\n const [readyState, setReadyState] = useState<0 | 1 | 2>(0);\n const [lastEventId, setLastEventId] = useState<string>(\"\");\n const [error, setError] = useState<any>();\n const [events, setEvents] = useState<EventSourceMessage[]>([]);\n\n const controllerRef = useRef<AbortController | null>(null);\n const retryCountRef = useRef<number>(0);\n const onOpenRef = useRef<UseEventSourceOptions[\"onopen\"]>(undefined);\n const onMessageRef = useRef<UseEventSourceOptions[\"onmessage\"]>(undefined);\n const onErrorRef = useRef<UseEventSourceOptions[\"onerror\"]>(undefined);\n const onCloseRef = useRef<UseEventSourceOptions[\"onclose\"]>(undefined);\n\n // Keep latest handlers without changing the start() identity\n useEffect(() => {\n onOpenRef.current = onOpen;\n }, [onOpen]);\n useEffect(() => {\n onMessageRef.current = onMessage;\n }, [onMessage]);\n useEffect(() => {\n onErrorRef.current = onError;\n }, [onError]);\n useEffect(() => {\n onCloseRef.current = onClose;\n }, [onClose]);\n\n const start = useCallback(() => {\n // Abort existing connection if any\n controllerRef.current?.abort();\n const controller = new AbortController();\n controllerRef.current = controller;\n\n setReadyState(0); // CONNECTING\n\n // Prepare RequestInit, honoring withCredentials via fetch credentials\n const requestInit: RequestInit = { ...init };\n if (withCredentials && !requestInit.credentials) {\n requestInit.credentials = \"include\";\n }\n\n // Normalize headers to plain object and include Last-Event-ID\n const headersRecord: Record<string, string> = {};\n const existing = new Headers(requestInit.headers ?? {});\n existing.forEach((value, key) => {\n headersRecord[key] = value;\n });\n if (lastEventId) {\n headersRecord[\"Last-Event-ID\"] = lastEventId;\n }\n\n fetchEventSource(url, {\n ...requestInit,\n headers: headersRecord,\n signal: controller.signal,\n fetch: customFetch,\n async onopen(response) {\n setReadyState(1); // OPEN\n retryCountRef.current = 0;\n onOpenRef.current?.(response);\n },\n onmessage(message) {\n setLastEventId(message.id ?? \"\");\n setEvents((prev) => [...prev, message]);\n onMessageRef.current?.(message);\n },\n onerror(err) {\n setError(err);\n onErrorRef.current?.(err);\n setReadyState(2); // CLOSED\n // retry logic\n if (retryIntervalMs != null) {\n const delay = Math.min(\n retryIntervalMs * 2 ** retryCountRef.current,\n maxRetryIntervalMs ?? Infinity\n );\n retryCountRef.current += 1;\n setTimeout(() => {\n if (!controller.signal.aborted) start();\n }, delay);\n } else {\n throw err;\n }\n },\n onclose() {\n setReadyState(2); // CLOSED\n onCloseRef.current?.();\n },\n });\n }, [\n url,\n init,\n customFetch,\n withCredentials,\n retryIntervalMs,\n maxRetryIntervalMs,\n ]);\n\n // Auto-start on mount and options change\n useEffect(() => {\n start();\n return () => {\n controllerRef.current?.abort();\n };\n }, [start]);\n\n // Pause/resume on visibility\n useEffect(() => {\n if (!pauseOnHidden) return;\n const handler = () => {\n if (document.hidden) {\n controllerRef.current?.abort();\n } else {\n start();\n }\n };\n document.addEventListener(\"visibilitychange\", handler);\n return () => {\n document.removeEventListener(\"visibilitychange\", handler);\n };\n }, [pauseOnHidden, start]);\n\n const close = useCallback(() => {\n controllerRef.current?.abort();\n setReadyState(2); // CLOSED\n }, []);\n\n const reconnect = useCallback(() => {\n start();\n }, [start]);\n\n return { readyState, lastEventId, error, events, close, reconnect };\n}\n"],"mappings":"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","names":["_a","_b"]}