feat: add heartbeat/ping to terminal WebSocket
- Backend: Send ping every 30s from WebSocket endpoint - Frontend: Respond to pings with pongs, detect missed pings (60s timeout) - Update type definitions to include 'resetting' status Refs: persistent-terminal-sessions task 6.4
This commit is contained in:
@@ -8,7 +8,7 @@ interface MobileTerminalHeaderProps {
|
||||
onClose?: () => void;
|
||||
onFontSizeChange?: (delta: number) => void;
|
||||
isVisible: boolean;
|
||||
connectionStatus?: "connecting" | "connected" | "disconnected" | "error";
|
||||
connectionStatus?: "connecting" | "connected" | "disconnected" | "error" | "resetting";
|
||||
}
|
||||
|
||||
export const MobileTerminalHeader: React.FC<MobileTerminalHeaderProps> = ({
|
||||
|
||||
@@ -30,7 +30,7 @@ export const MobileTerminalWrapper: React.FC<MobileTerminalWrapperProps> = ({
|
||||
const [activeModifier, setActiveModifier] = useState<ModifierKey | null>(null);
|
||||
const [terminalRef, setTerminalRef] = useState<{
|
||||
sendData: (data: string) => void;
|
||||
connectionStatus: "connecting" | "connected" | "disconnected" | "error";
|
||||
connectionStatus: "connecting" | "connected" | "disconnected" | "error" | "resetting";
|
||||
focusInput: () => void;
|
||||
changeFontSize: (delta: number) => void;
|
||||
} | null>(null);
|
||||
@@ -42,7 +42,7 @@ export const MobileTerminalWrapper: React.FC<MobileTerminalWrapperProps> = ({
|
||||
}, [headerAutoHide]);
|
||||
|
||||
const handleTerminalReady = useCallback(
|
||||
(sendData: (data: string) => void, connectionStatus: "connecting" | "connected" | "disconnected" | "error", focusInput: () => void, changeFontSize: (delta: number) => void) => {
|
||||
(sendData: (data: string) => void, connectionStatus: "connecting" | "connected" | "disconnected" | "error" | "resetting", focusInput: () => void, changeFontSize: (delta: number) => void) => {
|
||||
setTerminalRef({ sendData, connectionStatus, focusInput, changeFontSize });
|
||||
},
|
||||
[]
|
||||
|
||||
@@ -14,7 +14,7 @@ interface TerminalProps {
|
||||
onModifierChange?: (modifier: ModifierKey | null) => void;
|
||||
onTerminalReady?: (
|
||||
sendData: (data: string) => void,
|
||||
connectionStatus: "connecting" | "connected" | "disconnected" | "error",
|
||||
connectionStatus: "connecting" | "connected" | "disconnected" | "error" | "resetting",
|
||||
focusInput: () => void,
|
||||
changeFontSize: (delta: number) => void
|
||||
) => void;
|
||||
@@ -54,6 +54,8 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
const stored = localStorage.getItem(FONT_SIZE_KEY);
|
||||
return stored ? parseInt(stored, 10) : isMobile ? 16 : 14;
|
||||
});
|
||||
const lastPingRef = useRef<number>(0);
|
||||
const heartbeatCheckRef = useRef<number | null>(null);
|
||||
|
||||
const calculateFontSize = useCallback(() => {
|
||||
if (!isMobile) return fontSize;
|
||||
@@ -75,6 +77,20 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
setStatus("connected");
|
||||
setError(null);
|
||||
reconnectAttemptsRef.current = 0;
|
||||
lastPingRef.current = Date.now();
|
||||
|
||||
// Start heartbeat check
|
||||
if (heartbeatCheckRef.current) {
|
||||
window.clearInterval(heartbeatCheckRef.current);
|
||||
}
|
||||
heartbeatCheckRef.current = window.setInterval(() => {
|
||||
const elapsed = Date.now() - lastPingRef.current;
|
||||
if (elapsed > 60000) {
|
||||
// No ping for 60 seconds, connection may be dead
|
||||
console.warn("Terminal heartbeat timeout, reconnecting...");
|
||||
ws.close(4000, "Heartbeat timeout");
|
||||
}
|
||||
}, 30000);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
@@ -95,6 +111,12 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
} else if (msg.status === "resetting") {
|
||||
setStatus("resetting");
|
||||
}
|
||||
} else if (msg.type === "ping") {
|
||||
// Respond with pong and update last ping time
|
||||
lastPingRef.current = Date.now();
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: "pong" }));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
termRef.current?.write(event.data);
|
||||
@@ -104,6 +126,13 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
|
||||
ws.onclose = (event) => {
|
||||
setStatus("disconnected");
|
||||
|
||||
// Clean up heartbeat check
|
||||
if (heartbeatCheckRef.current) {
|
||||
window.clearInterval(heartbeatCheckRef.current);
|
||||
heartbeatCheckRef.current = null;
|
||||
}
|
||||
|
||||
if (event.code !== 1000) {
|
||||
setError(`Connection closed (code: ${event.code})`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user