From b21dee27c1d57209354cbfdf0b65100a36de1793 Mon Sep 17 00:00:00 2001 From: Benson Wong Date: Sun, 7 Sep 2025 21:48:58 -0700 Subject: [PATCH] Fix #288 Vite hot module reloading creating multiple SSE connections (#290) - move SSE (EventSource) connection to module level - manage EventSource as a singleton, closing open connection before reopening a new one --- ui/src/contexts/APIProvider.tsx | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/ui/src/contexts/APIProvider.tsx b/ui/src/contexts/APIProvider.tsx index 19a636b..aebc625 100644 --- a/ui/src/contexts/APIProvider.tsx +++ b/ui/src/contexts/APIProvider.tsx @@ -1,4 +1,4 @@ -import { useRef, createContext, useState, useContext, useEffect, useCallback, useMemo, type ReactNode } from "react"; +import { createContext, useState, useContext, useEffect, useCallback, useMemo, type ReactNode } from "react"; import type { ConnectionState } from "../lib/types"; type ModelStatus = "ready" | "starting" | "stopping" | "stopped" | "shutdown" | "unknown"; @@ -51,12 +51,14 @@ type APIProviderProps = { autoStartAPIEvents?: boolean; }; +let apiEventSource: EventSource | null = null; + export function APIProvider({ children, autoStartAPIEvents = true }: APIProviderProps) { const [proxyLogs, setProxyLogs] = useState(""); const [upstreamLogs, setUpstreamLogs] = useState(""); const [metrics, setMetrics] = useState([]); const [connectionStatus, setConnectionState] = useState("disconnected"); - const apiEventSource = useRef(null); + //const apiEventSource = useRef(null); const [models, setModels] = useState([]); @@ -69,8 +71,8 @@ export function APIProvider({ children, autoStartAPIEvents = true }: APIProvider const enableAPIEvents = useCallback((enabled: boolean) => { if (!enabled) { - apiEventSource.current?.close(); - apiEventSource.current = null; + apiEventSource?.close(); + apiEventSource = null; setMetrics([]); return; } @@ -79,22 +81,22 @@ export function APIProvider({ children, autoStartAPIEvents = true }: APIProvider const initialDelay = 1000; // 1 second const connect = () => { - apiEventSource.current = null; - const eventSource = new EventSource("/api/events"); + apiEventSource?.close(); + apiEventSource = new EventSource("/api/events"); + setConnectionState("connecting"); - eventSource.onopen = () => { + apiEventSource.onopen = () => { // clear everything out on connect to keep things in sync setProxyLogs(""); setUpstreamLogs(""); setMetrics([]); // clear metrics on reconnect setModels([]); // clear models on reconnect - apiEventSource.current = eventSource; retryCount = 0; setConnectionState("connected"); }; - eventSource.onmessage = (e: MessageEvent) => { + apiEventSource.onmessage = (e: MessageEvent) => { try { const message = JSON.parse(e.data) as APIEventEnvelope; switch (message.type) { @@ -137,8 +139,8 @@ export function APIProvider({ children, autoStartAPIEvents = true }: APIProvider } }; - eventSource.onerror = () => { - eventSource.close(); + apiEventSource.onerror = () => { + apiEventSource?.close(); retryCount++; const delay = Math.min(initialDelay * Math.pow(2, retryCount - 1), 5000); setConnectionState("disconnected");