[claude] App-state aware WebSocket reconnect on foreground (#33) #77
@@ -11,6 +11,7 @@ const STATUS_CONFIG: Record<ConnectionStatus, { color: string; label: string }>
|
||||
connecting: { color: "#F59E0B", label: "Connecting" },
|
||||
connected: { color: "#10B981", label: "Live" },
|
||||
disconnected: { color: "#6B7280", label: "Offline" },
|
||||
reconnecting: { color: "#F59E0B", label: "Reconnecting" },
|
||||
error: { color: "#EF4444", label: "Error" },
|
||||
};
|
||||
|
||||
@@ -18,7 +19,7 @@ export function ConnectionBadge({ status }: { status: ConnectionStatus }) {
|
||||
const pulseAnim = useRef(new Animated.Value(1)).current;
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "connecting") {
|
||||
if (status === "connecting" || status === "reconnecting") {
|
||||
const pulse = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnim, { toValue: 0.3, duration: 600, useNativeDriver: true }),
|
||||
|
||||
@@ -7,6 +7,7 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { AppState, Platform } from "react-native";
|
||||
|
||||
export type TimmyMood = "idle" | "thinking" | "working" | "speaking";
|
||||
|
||||
@@ -21,7 +22,7 @@ export type WsEvent = {
|
||||
count?: number;
|
||||
};
|
||||
|
||||
export type ConnectionStatus = "connecting" | "connected" | "disconnected" | "error";
|
||||
export type ConnectionStatus = "connecting" | "connected" | "disconnected" | "reconnecting" | "error";
|
||||
|
||||
type TimmyContextValue = {
|
||||
timmyMood: TimmyMood;
|
||||
@@ -215,6 +216,54 @@ export function TimmyProvider({ children }: { children: React.ReactNode }) {
|
||||
};
|
||||
}, [connectWs]);
|
||||
|
||||
// AppState-aware WebSocket reconnect on foreground
|
||||
useEffect(() => {
|
||||
if (Platform.OS === "web") return;
|
||||
|
||||
const appStateRef = { current: AppState.currentState };
|
||||
|
||||
const subscription = AppState.addEventListener("change", (nextAppState) => {
|
||||
const wasBackground =
|
||||
appStateRef.current === "background" ||
|
||||
appStateRef.current === "inactive";
|
||||
const isNowActive = nextAppState === "active";
|
||||
|
||||
if (wasBackground && isNowActive) {
|
||||
// App returned to foreground — check if WS is still alive
|
||||
const ws = wsRef.current;
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
// Cancel any pending retry so we don't create duplicates
|
||||
if (retryTimerRef.current) {
|
||||
clearTimeout(retryTimerRef.current);
|
||||
retryTimerRef.current = null;
|
||||
}
|
||||
retryCountRef.current = 0;
|
||||
setConnectionStatus("reconnecting");
|
||||
connectWs();
|
||||
}
|
||||
} else if (nextAppState === "background") {
|
||||
// Proactively close the WS to avoid OS killing it mid-frame
|
||||
if (retryTimerRef.current) {
|
||||
clearTimeout(retryTimerRef.current);
|
||||
retryTimerRef.current = null;
|
||||
}
|
||||
if (wsRef.current) {
|
||||
wsRef.current.onclose = null;
|
||||
wsRef.current.onerror = null;
|
||||
wsRef.current.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
setConnectionStatus("disconnected");
|
||||
}
|
||||
|
||||
appStateRef.current = nextAppState;
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
}, [connectWs]);
|
||||
|
||||
const send = useCallback((msg: object) => {
|
||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
||||
wsRef.current.send(JSON.stringify(msg));
|
||||
|
||||
Reference in New Issue
Block a user