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
This commit is contained in:
Benson Wong
2025-09-07 21:48:58 -07:00
committed by GitHub
parent f58c8c8ec5
commit b21dee27c1

View File

@@ -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"; import type { ConnectionState } from "../lib/types";
type ModelStatus = "ready" | "starting" | "stopping" | "stopped" | "shutdown" | "unknown"; type ModelStatus = "ready" | "starting" | "stopping" | "stopped" | "shutdown" | "unknown";
@@ -51,12 +51,14 @@ type APIProviderProps = {
autoStartAPIEvents?: boolean; autoStartAPIEvents?: boolean;
}; };
let apiEventSource: EventSource | null = null;
export function APIProvider({ children, autoStartAPIEvents = true }: APIProviderProps) { export function APIProvider({ children, autoStartAPIEvents = true }: APIProviderProps) {
const [proxyLogs, setProxyLogs] = useState(""); const [proxyLogs, setProxyLogs] = useState("");
const [upstreamLogs, setUpstreamLogs] = useState(""); const [upstreamLogs, setUpstreamLogs] = useState("");
const [metrics, setMetrics] = useState<Metrics[]>([]); const [metrics, setMetrics] = useState<Metrics[]>([]);
const [connectionStatus, setConnectionState] = useState<ConnectionState>("disconnected"); const [connectionStatus, setConnectionState] = useState<ConnectionState>("disconnected");
const apiEventSource = useRef<EventSource | null>(null); //const apiEventSource = useRef<EventSource | null>(null);
const [models, setModels] = useState<Model[]>([]); const [models, setModels] = useState<Model[]>([]);
@@ -69,8 +71,8 @@ export function APIProvider({ children, autoStartAPIEvents = true }: APIProvider
const enableAPIEvents = useCallback((enabled: boolean) => { const enableAPIEvents = useCallback((enabled: boolean) => {
if (!enabled) { if (!enabled) {
apiEventSource.current?.close(); apiEventSource?.close();
apiEventSource.current = null; apiEventSource = null;
setMetrics([]); setMetrics([]);
return; return;
} }
@@ -79,22 +81,22 @@ export function APIProvider({ children, autoStartAPIEvents = true }: APIProvider
const initialDelay = 1000; // 1 second const initialDelay = 1000; // 1 second
const connect = () => { const connect = () => {
apiEventSource.current = null; apiEventSource?.close();
const eventSource = new EventSource("/api/events"); apiEventSource = new EventSource("/api/events");
setConnectionState("connecting"); setConnectionState("connecting");
eventSource.onopen = () => { apiEventSource.onopen = () => {
// clear everything out on connect to keep things in sync // clear everything out on connect to keep things in sync
setProxyLogs(""); setProxyLogs("");
setUpstreamLogs(""); setUpstreamLogs("");
setMetrics([]); // clear metrics on reconnect setMetrics([]); // clear metrics on reconnect
setModels([]); // clear models on reconnect setModels([]); // clear models on reconnect
apiEventSource.current = eventSource;
retryCount = 0; retryCount = 0;
setConnectionState("connected"); setConnectionState("connected");
}; };
eventSource.onmessage = (e: MessageEvent) => { apiEventSource.onmessage = (e: MessageEvent) => {
try { try {
const message = JSON.parse(e.data) as APIEventEnvelope; const message = JSON.parse(e.data) as APIEventEnvelope;
switch (message.type) { switch (message.type) {
@@ -137,8 +139,8 @@ export function APIProvider({ children, autoStartAPIEvents = true }: APIProvider
} }
}; };
eventSource.onerror = () => { apiEventSource.onerror = () => {
eventSource.close(); apiEventSource?.close();
retryCount++; retryCount++;
const delay = Math.min(initialDelay * Math.pow(2, retryCount - 1), 5000); const delay = Math.min(initialDelay * Math.pow(2, retryCount - 1), 5000);
setConnectionState("disconnected"); setConnectionState("disconnected");