Cleanup error handling

This commit is contained in:
Daniel Pupius
2025-04-25 09:00:00 -07:00
parent 86e6f45d5e
commit 4a41b0062d
4 changed files with 340 additions and 240 deletions
+172 -46
View File
@@ -8,6 +8,7 @@ import {
StreamEventHandler,
InfoEvent,
MessageEvent,
PaddingEvent,
BadDataEvent,
} from "./types";
@@ -22,63 +23,188 @@ export type {
MessageEvent,
BadDataEvent,
StreamEvent,
PaddingEvent,
StreamEventHandler,
};
/**
* Establish a Server-Sent Events connection to receive real-time packets
* with automatic reconnection
*
* @param onEvent Handler function for stream events (messages, info, errors)
* @param onError Optional handler for connection errors
* @returns A function that closes the connection when called
*/
export function streamPackets(
onEvent: StreamEventHandler,
onError?: (error: Event) => void
): () => void {
const evtSource = new EventSource(API_ENDPOINTS.STREAM);
console.log("[SSE] Connecting to stream...", evtSource);
// Handle info events specifically
evtSource.addEventListener("info", (event) => {
// Connection state
let source: EventSource | null = null;
let reconnectTimer: number | null = null;
let shouldReconnect = true;
let reconnectAttempt = 0;
// Reconnection settings
const INITIAL_RECONNECT_DELAY = 1000; // 1 second
const MAX_RECONNECT_DELAY = 30000; // 30 seconds
const MAX_RECONNECT_ATTEMPTS = 30; // Give up after this many attempts
/**
* Calculate delay for exponential backoff
*/
function getReconnectDelay(): number {
// Calculate exponential backoff: 1s, 2s, 4s, 8s, etc. up to max
const delay = Math.min(
INITIAL_RECONNECT_DELAY * Math.pow(2, reconnectAttempt),
MAX_RECONNECT_DELAY
);
return delay;
}
/**
* Clean up the current connection and timers
*/
function cleanup(): void {
// Clear any pending reconnect timer
if (reconnectTimer !== null) {
window.clearTimeout(reconnectTimer);
reconnectTimer = null;
}
// Close and cleanup the event source if it exists
if (source !== null) {
// Remove all event listeners
source.removeEventListener("message", handleMessage as EventListener);
source.removeEventListener("info", handleInfo as EventListener);
source.onerror = null;
// Close the connection
source.close();
source = null;
}
}
/**
* Handle info events
*/
function handleInfo(event: Event): void {
const evtData = (event as any).data;
// If we receive an info event, connection is working
if (reconnectAttempt > 0) {
console.log("[SSE] Connection restored");
reconnectAttempt = 0;
}
// Forward the event to the caller
onEvent({
type: "info",
data: event.data,
});
});
// Handle message events specifically
evtSource.addEventListener("message", (event) => {
handleEventData(event.data, onEvent);
});
// Handle errors
if (onError) {
evtSource.onerror = onError;
} else {
evtSource.onerror = () => {
console.error("EventSource failed");
evtSource.close();
};
}
// Return cleanup function
return () => evtSource.close();
}
/**
* Helper to handle event data based on type
*/
function handleEventData(data: string, callback: StreamEventHandler): void {
try {
const parsedData = JSON.parse(data) as Packet;
callback({
type: "message",
data: parsedData,
});
} catch (error) {
console.warn("Failed to parse message as JSON:", error, "Raw data:", data);
callback({
type: "bad_data",
data,
data: String(evtData),
});
}
}
/**
* Handle message events
*/
function handleMessage(event: Event): void {
const evtData = (event as any).data;
try {
// Parse the event data as JSON
const parsedData = JSON.parse(String(evtData)) as Packet;
// Forward the message to the caller
onEvent({
type: "message",
data: parsedData,
});
} catch (error) {
console.warn("[SSE] Failed to parse message:", error);
// Forward bad data to the caller
onEvent({
type: "bad_data",
data: String(evtData),
});
}
}
/**
* Handle connection errors and reconnection
*/
function handleError(event: Event): void {
// Log the error
console.error("[SSE] Connection error");
// Notify the caller
if (onError) {
onError(event);
}
// Clean up the current connection
cleanup();
// Reconnect if we should
if (shouldReconnect && reconnectAttempt < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempt++;
// Calculate backoff delay
const delay = getReconnectDelay();
console.log(`[SSE] Reconnecting in ${delay}ms (attempt ${reconnectAttempt})`);
// Schedule reconnection
reconnectTimer = window.setTimeout(connect, delay);
} else if (reconnectAttempt >= MAX_RECONNECT_ATTEMPTS) {
console.error(`[SSE] Giving up after ${MAX_RECONNECT_ATTEMPTS} failed reconnection attempts`);
}
}
/**
* Connect to the event stream
*/
function connect(): void {
// Clean up any existing connection first
cleanup();
// Don't continue if we're no longer supposed to reconnect
if (!shouldReconnect) {
return;
}
try {
// Create a new EventSource connection
source = new EventSource(API_ENDPOINTS.STREAM);
// Log connection attempt
if (reconnectAttempt === 0) {
console.log("[SSE] Connecting to stream");
} else {
console.log(`[SSE] Reconnecting to stream (attempt ${reconnectAttempt})`);
}
// Set up event handlers
source.addEventListener("info", handleInfo as EventListener);
source.addEventListener("message", handleMessage as EventListener);
source.onerror = handleError;
} catch (error) {
console.error("[SSE] Failed to create EventSource:", error);
handleError(new Event("error"));
}
}
// Start the initial connection
connect();
/**
* Return a function that will close the connection when called
*/
return function close(): void {
console.log("[SSE] Closing connection permanently");
// Set flag to prevent reconnection
shouldReconnect = false;
// Clean up resources
cleanup();
};
}
+121 -112
View File
@@ -37,153 +37,153 @@ export enum PortNum {
// Map of PortNum string names to numeric enum values
export const PortNumByName: Record<string, PortNum> = {
"UNKNOWN_APP": PortNum.UNKNOWN_APP,
"TEXT_MESSAGE_APP": PortNum.TEXT_MESSAGE_APP,
"REMOTE_HARDWARE_APP": PortNum.REMOTE_HARDWARE_APP,
"POSITION_APP": PortNum.POSITION_APP,
"NODEINFO_APP": PortNum.NODEINFO_APP,
"ROUTING_APP": PortNum.ROUTING_APP,
"ADMIN_APP": PortNum.ADMIN_APP,
"TEXT_MESSAGE_COMPRESSED_APP": PortNum.TEXT_MESSAGE_COMPRESSED_APP,
"WAYPOINT_APP": PortNum.WAYPOINT_APP,
"AUDIO_APP": PortNum.AUDIO_APP,
"REPLY_APP": PortNum.REPLY_APP,
"IP_TUNNEL_APP": PortNum.IP_TUNNEL_APP,
"SERIAL_APP": PortNum.SERIAL_APP,
"STORE_FORWARD_APP": PortNum.STORE_FORWARD_APP,
"RANGE_TEST_APP": PortNum.RANGE_TEST_APP,
"TELEMETRY_APP": PortNum.TELEMETRY_APP,
"PRIVATE_APP": PortNum.PRIVATE_APP,
"DETECTION_SENSOR_APP": PortNum.DETECTION_SENSOR_APP,
"ZPS_APP": PortNum.ZPS_APP,
"SIMULATOR_APP": PortNum.SIMULATOR_APP,
"TRACEROUTE_APP": PortNum.TRACEROUTE_APP,
"NEIGHBORINFO_APP": PortNum.NEIGHBORINFO_APP,
"PAXCOUNTER_APP": PortNum.PAXCOUNTER_APP,
"MAP_REPORT_APP": PortNum.MAP_REPORT_APP,
"ALERT_APP": PortNum.ALERT_APP,
"ATAK_PLUGIN": PortNum.ATAK_PLUGIN,
"POWERSTRESS_APP": PortNum.POWERSTRESS_APP,
"RETICULUM_TUNNEL_APP": PortNum.RETICULUM_TUNNEL_APP,
}
UNKNOWN_APP: PortNum.UNKNOWN_APP,
TEXT_MESSAGE_APP: PortNum.TEXT_MESSAGE_APP,
REMOTE_HARDWARE_APP: PortNum.REMOTE_HARDWARE_APP,
POSITION_APP: PortNum.POSITION_APP,
NODEINFO_APP: PortNum.NODEINFO_APP,
ROUTING_APP: PortNum.ROUTING_APP,
ADMIN_APP: PortNum.ADMIN_APP,
TEXT_MESSAGE_COMPRESSED_APP: PortNum.TEXT_MESSAGE_COMPRESSED_APP,
WAYPOINT_APP: PortNum.WAYPOINT_APP,
AUDIO_APP: PortNum.AUDIO_APP,
REPLY_APP: PortNum.REPLY_APP,
IP_TUNNEL_APP: PortNum.IP_TUNNEL_APP,
SERIAL_APP: PortNum.SERIAL_APP,
STORE_FORWARD_APP: PortNum.STORE_FORWARD_APP,
RANGE_TEST_APP: PortNum.RANGE_TEST_APP,
TELEMETRY_APP: PortNum.TELEMETRY_APP,
PRIVATE_APP: PortNum.PRIVATE_APP,
DETECTION_SENSOR_APP: PortNum.DETECTION_SENSOR_APP,
ZPS_APP: PortNum.ZPS_APP,
SIMULATOR_APP: PortNum.SIMULATOR_APP,
TRACEROUTE_APP: PortNum.TRACEROUTE_APP,
NEIGHBORINFO_APP: PortNum.NEIGHBORINFO_APP,
PAXCOUNTER_APP: PortNum.PAXCOUNTER_APP,
MAP_REPORT_APP: PortNum.MAP_REPORT_APP,
ALERT_APP: PortNum.ALERT_APP,
ATAK_PLUGIN: PortNum.ATAK_PLUGIN,
POWERSTRESS_APP: PortNum.POWERSTRESS_APP,
RETICULUM_TUNNEL_APP: PortNum.RETICULUM_TUNNEL_APP,
};
// Different payload types based on the Meshtastic protocol
export interface Position {
latitudeI?: number; // multiply by 1e-7 to get degrees
longitudeI?: number; // multiply by 1e-7 to get degrees
altitude?: number; // in meters above MSL
time: number; // seconds since 1970
locationSource?: string; // "LOC_UNSET", "LOC_MANUAL", "LOC_INTERNAL", "LOC_EXTERNAL"
altitudeSource?: string; // "ALT_UNSET", "ALT_MANUAL", "ALT_INTERNAL", "ALT_EXTERNAL", "ALT_BAROMETRIC"
timestamp?: number; // positional timestamp in integer epoch seconds
latitudeI?: number; // multiply by 1e-7 to get degrees
longitudeI?: number; // multiply by 1e-7 to get degrees
altitude?: number; // in meters above MSL
time: number; // seconds since 1970
locationSource?: string; // "LOC_UNSET", "LOC_MANUAL", "LOC_INTERNAL", "LOC_EXTERNAL"
altitudeSource?: string; // "ALT_UNSET", "ALT_MANUAL", "ALT_INTERNAL", "ALT_EXTERNAL", "ALT_BAROMETRIC"
timestamp?: number; // positional timestamp in integer epoch seconds
timestampMillisAdjust?: number;
altitudeHae?: number; // HAE altitude in meters
altitudeHae?: number; // HAE altitude in meters
altitudeGeoSeparation?: number;
pdop?: number; // Position Dilution of Precision, in 1/100 units
hdop?: number; // Horizontal Dilution of Precision
vdop?: number; // Vertical Dilution of Precision
gpsAccuracy?: number; // GPS accuracy in mm
groundSpeed?: number; // in m/s
groundTrack?: number; // True North track in 1/100 degrees
fixQuality?: number; // GPS fix quality
fixType?: number; // GPS fix type 2D/3D
satsInView?: number; // Satellites in view
sensorId?: number; // For multiple positioning sensors
nextUpdate?: number; // Estimated time until next update in seconds
seqNumber?: number; // Sequence number for this packet
precisionBits?: number; // Bits of precision set by the sending node
pdop?: number; // Position Dilution of Precision, in 1/100 units
hdop?: number; // Horizontal Dilution of Precision
vdop?: number; // Vertical Dilution of Precision
gpsAccuracy?: number; // GPS accuracy in mm
groundSpeed?: number; // in m/s
groundTrack?: number; // True North track in 1/100 degrees
fixQuality?: number; // GPS fix quality
fixType?: number; // GPS fix type 2D/3D
satsInView?: number; // Satellites in view
sensorId?: number; // For multiple positioning sensors
nextUpdate?: number; // Estimated time until next update in seconds
seqNumber?: number; // Sequence number for this packet
precisionBits?: number; // Bits of precision set by the sending node
}
export interface User {
id: string; // Globally unique ID for this user
longName?: string; // Full name for this user
shortName?: string; // Very short name, ideally two characters
macaddr?: string; // MAC address of the device
hwModel?: string; // Hardware model name
hasGps?: boolean; // Whether the node has GPS capability
role?: string; // User's role in the mesh (e.g., "ROUTER")
snr?: number; // Signal-to-noise ratio
batteryLevel?: number; // Battery level 0-100
voltage?: number; // Battery voltage
id: string; // Globally unique ID for this user
longName?: string; // Full name for this user
shortName?: string; // Very short name, ideally two characters
macaddr?: string; // MAC address of the device
hwModel?: string; // Hardware model name
hasGps?: boolean; // Whether the node has GPS capability
role?: string; // User's role in the mesh (e.g., "ROUTER")
snr?: number; // Signal-to-noise ratio
batteryLevel?: number; // Battery level 0-100
voltage?: number; // Battery voltage
channelUtilization?: number; // Channel utilization percentage
airUtilTx?: number; // Air utilization for transmission
lastHeard?: number; // Last time the node was heard from
airUtilTx?: number; // Air utilization for transmission
lastHeard?: number; // Last time the node was heard from
}
// Device metrics for telemetry
export interface DeviceMetrics {
batteryLevel?: number; // 0-100 (>100 means powered)
voltage?: number; // Voltage measured
batteryLevel?: number; // 0-100 (>100 means powered)
voltage?: number; // Voltage measured
channelUtilization?: number;
airUtilTx?: number; // Percent of airtime for transmission used within the last hour
uptimeSeconds?: number; // How long the device has been running since last reboot (in seconds)
airUtilTx?: number; // Percent of airtime for transmission used within the last hour
uptimeSeconds?: number; // How long the device has been running since last reboot (in seconds)
}
// Environment metrics for telemetry
export interface EnvironmentMetrics {
temperature?: number; // Temperature measured
temperature?: number; // Temperature measured
relativeHumidity?: number; // Relative humidity percent measured
barometricPressure?: number; // Barometric pressure in hPA
gasResistance?: number; // Gas resistance in MOhm
voltage?: number; // Voltage measured (To be deprecated)
current?: number; // Current measured (To be deprecated)
iaq?: number; // IAQ value (0-500)
distance?: number; // Distance in mm (water level detection)
lux?: number; // Ambient light (Lux)
whiteLux?: number; // White light (irradiance)
irLux?: number; // Infrared lux
uvLux?: number; // Ultraviolet lux
windDirection?: number; // Wind direction in degrees (0 = North, 90 = East)
windSpeed?: number; // Wind speed in m/s
weight?: number; // Weight in KG
windGust?: number; // Wind gust in m/s
windLull?: number; // Wind lull in m/s
radiation?: number; // Radiation in µR/h
rainfall1h?: number; // Rainfall in the last hour in mm
rainfall24h?: number; // Rainfall in the last 24 hours in mm
soilMoisture?: number; // Soil moisture measured (% 1-100)
gasResistance?: number; // Gas resistance in MOhm
voltage?: number; // Voltage measured (To be deprecated)
current?: number; // Current measured (To be deprecated)
iaq?: number; // IAQ value (0-500)
distance?: number; // Distance in mm (water level detection)
lux?: number; // Ambient light (Lux)
whiteLux?: number; // White light (irradiance)
irLux?: number; // Infrared lux
uvLux?: number; // Ultraviolet lux
windDirection?: number; // Wind direction in degrees (0 = North, 90 = East)
windSpeed?: number; // Wind speed in m/s
weight?: number; // Weight in KG
windGust?: number; // Wind gust in m/s
windLull?: number; // Wind lull in m/s
radiation?: number; // Radiation in µR/h
rainfall1h?: number; // Rainfall in the last hour in mm
rainfall24h?: number; // Rainfall in the last 24 hours in mm
soilMoisture?: number; // Soil moisture measured (% 1-100)
soilTemperature?: number; // Soil temperature measured (°C)
}
// Main telemetry interface
export interface Telemetry {
time: number; // seconds since 1970
time: number; // seconds since 1970
deviceMetrics?: DeviceMetrics;
environmentMetrics?: EnvironmentMetrics;
// Other telemetry types could be added as needed (PowerMetrics, AirQualityMetrics, etc.)
}
export interface Waypoint {
id: number; // ID of the waypoint
latitudeI?: number; // multiply by 1e-7 to get degrees
longitudeI?: number; // multiply by 1e-7 to get degrees
expire?: number; // Time the waypoint is to expire (epoch)
lockedTo?: number; // If greater than zero, nodenum allowed to update the waypoint
name?: string; // Name of the waypoint - max 30 chars
description?: string; // Description of the waypoint - max 100 chars
icon?: number; // Designator icon for the waypoint (unicode emoji)
id: number; // ID of the waypoint
latitudeI?: number; // multiply by 1e-7 to get degrees
longitudeI?: number; // multiply by 1e-7 to get degrees
expire?: number; // Time the waypoint is to expire (epoch)
lockedTo?: number; // If greater than zero, nodenum allowed to update the waypoint
name?: string; // Name of the waypoint - max 30 chars
description?: string; // Description of the waypoint - max 100 chars
icon?: number; // Designator icon for the waypoint (unicode emoji)
}
export interface RouteDiscovery {
route?: number[]; // The list of nodenums this packet has visited
snrTowards?: number[]; // The list of SNRs (in dB, scaled by 4) in the route towards destination
routeBack?: number[]; // The list of nodenums the packet has visited on the way back
snrBack?: number[]; // The list of SNRs (in dB, scaled by 4) in the route back
route?: number[]; // The list of nodenums this packet has visited
snrTowards?: number[]; // The list of SNRs (in dB, scaled by 4) in the route towards destination
routeBack?: number[]; // The list of nodenums the packet has visited on the way back
snrBack?: number[]; // The list of SNRs (in dB, scaled by 4) in the route back
}
// A neighbor in the mesh
export interface Neighbor {
nodeId: number; // Node ID of neighbor
snr: number; // SNR of last heard message
lastRxTime?: number; // Reception time of last message
nodeId: number; // Node ID of neighbor
snr: number; // SNR of last heard message
lastRxTime?: number; // Reception time of last message
nodeBroadcastIntervalSecs?: number; // Broadcast interval of this neighbor
}
export interface NeighborInfo {
nodeId: number; // The node ID of the node sending info on its neighbors
lastSentById?: number; // Field to pass neighbor info for the next sending cycle
nodeId: number; // The node ID of the node sending info on its neighbors
lastSentById?: number; // Field to pass neighbor info for the next sending cycle
nodeBroadcastIntervalSecs?: number; // Broadcast interval of the represented node
neighbors?: Neighbor[]; // The list of out edges from this node
neighbors?: Neighbor[]; // The list of out edges from this node
}
export interface MapReport {
@@ -217,13 +217,13 @@ export enum RoutingError {
PKI_FAILED = 34,
PKI_UNKNOWN_PUBKEY = 35,
ADMIN_BAD_SESSION_KEY = 36,
ADMIN_PUBLIC_KEY_UNAUTHORIZED = 37
ADMIN_PUBLIC_KEY_UNAUTHORIZED = 37,
}
export interface Routing {
routeRequest?: RouteDiscovery; // A route request going from the requester
routeReply?: RouteDiscovery; // A route reply
errorReason?: RoutingError; // A failure in delivering a message
routeRequest?: RouteDiscovery; // A route request going from the requester
routeReply?: RouteDiscovery; // A route reply
errorReason?: RoutingError; // A failure in delivering a message
}
export interface AdminMessage {
@@ -268,7 +268,7 @@ export interface Data {
// From Data
portNum: PortNum | string; // Can be either enum value or string name
// Payload is one of these types, depending on portNum
textMessage?: string;
binaryData?: string; // Base64 encoded binary data
@@ -309,7 +309,7 @@ export interface Data {
// Error tracking
decodeError?: string;
// Reception timestamp (added by decoder)
rxTime?: number;
}
@@ -338,6 +338,15 @@ export interface BadDataEvent {
data: string; // Raw data that failed to parse
}
export type StreamEvent = InfoEvent | MessageEvent | BadDataEvent;
export interface PaddingEvent {
type: "padding";
data: string; // Random data to trigger a flush.
}
export type StreamEventHandler = (event: StreamEvent) => void;
export type StreamEvent =
| InfoEvent
| MessageEvent
| PaddingEvent
| BadDataEvent;
export type StreamEventHandler = (event: StreamEvent) => void;
+45 -80
View File
@@ -1,5 +1,5 @@
import { Outlet } from "@tanstack/react-router";
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { useAppDispatch } from "../hooks";
import { Nav } from "../components";
import { streamPackets, StreamEvent } from "../lib/api";
@@ -13,92 +13,57 @@ export const Route = createRootRoute({
function RootLayout() {
const dispatch = useAppDispatch();
const [connectionStatus, setConnectionStatus] = useState<string>("Connecting...");
const [connectionAttempts, setConnectionAttempts] = useState(0);
const [isReconnecting, setIsReconnecting] = useState(false);
const [connectionStatus, setConnectionStatus] =
useState<string>("Connecting...");
// Use refs instead of state for tracking reconnection status
// This way changes to these values won't trigger useEffect reruns
const connectionAttemptsRef = useRef(0);
const isReconnectingRef = useRef(false);
useEffect(() => {
// Connection management
let reconnectTimer: number | null = null;
const MAX_RECONNECT_DELAY = 10000; // Maximum reconnect delay in ms (10 seconds)
const INITIAL_RECONNECT_DELAY = 1000; // Start with 1 second
console.log("[SSE] Setting up event source (should happen only once)");
// Calculate exponential backoff delay with a cap
const getReconnectDelay = (attempts: number) => {
const delay = Math.min(
INITIAL_RECONNECT_DELAY * Math.pow(1.5, attempts),
MAX_RECONNECT_DELAY
);
return delay;
};
// Set up Server-Sent Events connection
const cleanup = streamPackets(
// Event handler for all event types
(event: StreamEvent) => {
if (event.type === "info") {
// Handle info events (connection status, etc.)
console.log(`[SSE] Info: ${event.data}`);
setConnectionStatus(event.data);
const connectToEventStream = () => {
// Log connection attempt
const attemptNum = connectionAttempts + 1;
console.log(`[SSE] Connection attempt ${attemptNum}${isReconnecting ? ' (reconnecting)' : ''}`);
// Set up Server-Sent Events connection
const cleanup = streamPackets(
// Event handler for all event types
(event: StreamEvent) => {
if (event.type === "info") {
// Handle info events (connection status, etc.)
console.log(`[SSE] Info: ${event.data}`);
setConnectionStatus(event.data);
// Reset connection attempts on successful connection
if (event.data.includes("Connected") && isReconnecting) {
console.log("[SSE] Connection restored successfully");
setConnectionAttempts(0);
setIsReconnecting(false);
}
} else if (event.type === "message") {
// Process message for both the aggregator and packet display
dispatch(processNewPacket(event.data));
dispatch(addPacket(event.data));
} else if (event.type === "bad_data") {
console.warn("[SSE] Received bad data:", event.data);
// Reset connection attempts on successful connection
if (event.data.includes("Connected") && isReconnectingRef.current) {
console.log("[SSE] Connection restored successfully");
connectionAttemptsRef.current = 0;
isReconnectingRef.current = false;
}
},
// On error handler
(error) => {
console.error("[SSE] Connection error:", error);
setConnectionStatus("Connection error. Reconnecting...");
setIsReconnecting(true);
// Increment connection attempts
const newAttempts = connectionAttempts + 1;
setConnectionAttempts(newAttempts);
// Calculate backoff delay
const reconnectDelay = getReconnectDelay(newAttempts);
console.log(`[SSE] Will attempt to reconnect in ${reconnectDelay}ms (attempt ${newAttempts})`);
// Close the current connection
cleanup();
// Schedule reconnection
reconnectTimer = window.setTimeout(() => {
connectToEventStream();
}, reconnectDelay);
} else if (event.type === "message") {
// Process message for both the aggregator and packet display
dispatch(processNewPacket(event.data));
dispatch(addPacket(event.data));
} else if (event.type === "bad_data") {
console.warn("[SSE] Received bad data:", event.data);
}
);
// Return cleanup function
return () => {
if (reconnectTimer) {
window.clearTimeout(reconnectTimer);
}
cleanup();
};
};
},
// On error handler - only for reporting UI state
(error) => {
console.error("[SSE] Connection error:", error);
setConnectionStatus("Connection error. Reconnecting...");
isReconnectingRef.current = true;
// Initial connection
const cleanupFn = connectToEventStream();
// Cleanup when component unmounts
return cleanupFn;
}, [dispatch, connectionAttempts, isReconnecting]);
// Increment connection attempts for UI tracking
connectionAttemptsRef.current += 1;
console.log(
`[SSE] Connection attempt ${connectionAttemptsRef.current}`
);
}
);
// This cleanup will only be called when the component unmounts
return cleanup;
}, [dispatch]);
return (
<div className="flex h-screen overflow-hidden bg-neutral-900">