fix(ws-error): add exponential reconnect mechanism
This commit is contained in:
parent
409c811a42
commit
5526d5f60f
1 changed files with 82 additions and 20 deletions
|
@ -9,7 +9,7 @@ import crypto from 'crypto';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { getSuggestions } from '@/lib/actions';
|
import { getSuggestions } from '@/lib/actions';
|
||||||
import Error from 'next/error';
|
import NextError from 'next/error';
|
||||||
|
|
||||||
export type Message = {
|
export type Message = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
|
@ -32,11 +32,24 @@ const useSocket = (
|
||||||
setIsWSReady: (ready: boolean) => void,
|
setIsWSReady: (ready: boolean) => void,
|
||||||
setError: (error: boolean) => void,
|
setError: (error: boolean) => void,
|
||||||
) => {
|
) => {
|
||||||
const [ws, setWs] = useState<WebSocket | null>(null);
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout>();
|
||||||
|
const retryCountRef = useRef(0);
|
||||||
|
const isCleaningUpRef = useRef(false);
|
||||||
|
const MAX_RETRIES = 3;
|
||||||
|
const INITIAL_BACKOFF = 1000; // 1 second
|
||||||
|
|
||||||
|
const getBackoffDelay = (retryCount: number) => {
|
||||||
|
return Math.min(INITIAL_BACKOFF * Math.pow(2, retryCount), 10000); // Cap at 10 seconds
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ws) {
|
|
||||||
const connectWs = async () => {
|
const connectWs = async () => {
|
||||||
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
|
wsRef.current.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
let chatModel = localStorage.getItem('chatModel');
|
let chatModel = localStorage.getItem('chatModel');
|
||||||
let chatModelProvider = localStorage.getItem('chatModelProvider');
|
let chatModelProvider = localStorage.getItem('chatModelProvider');
|
||||||
let embeddingModel = localStorage.getItem('embeddingModel');
|
let embeddingModel = localStorage.getItem('embeddingModel');
|
||||||
|
@ -59,7 +72,10 @@ const useSocket = (
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
).then(async (res) => await res.json());
|
).then(async (res) => {
|
||||||
|
if (!res.ok) throw new Error(`Failed to fetch models: ${res.status} ${res.statusText}`);
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!chatModel ||
|
!chatModel ||
|
||||||
|
@ -202,6 +218,7 @@ const useSocket = (
|
||||||
wsURL.search = searchParams.toString();
|
wsURL.search = searchParams.toString();
|
||||||
|
|
||||||
const ws = new WebSocket(wsURL.toString());
|
const ws = new WebSocket(wsURL.toString());
|
||||||
|
wsRef.current = ws;
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
if (ws.readyState !== 1) {
|
if (ws.readyState !== 1) {
|
||||||
|
@ -217,11 +234,14 @@ const useSocket = (
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (ws.readyState === 1) {
|
if (ws.readyState === 1) {
|
||||||
setIsWSReady(true);
|
setIsWSReady(true);
|
||||||
|
retryCountRef.current = 0;
|
||||||
|
setError(false);
|
||||||
|
toast.success('Connection restored');
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
}
|
}
|
||||||
}, 5);
|
}, 5);
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
console.log('[DEBUG] opened');
|
console.debug(new Date(), 'ws:connected');
|
||||||
}
|
}
|
||||||
if (data.type === 'error') {
|
if (data.type === 'error') {
|
||||||
toast.error(data.data);
|
toast.error(data.data);
|
||||||
|
@ -230,24 +250,62 @@ const useSocket = (
|
||||||
|
|
||||||
ws.onerror = () => {
|
ws.onerror = () => {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
setError(true);
|
setIsWSReady(false);
|
||||||
toast.error('WebSocket connection error.');
|
toast.error('WebSocket connection error.');
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
setError(true);
|
setIsWSReady(false);
|
||||||
console.log('[DEBUG] closed');
|
console.debug(new Date(), 'ws:disconnected');
|
||||||
|
if (!isCleaningUpRef.current) {
|
||||||
|
toast.error('Connection lost. Attempting to reconnect...');
|
||||||
|
attemptReconnect();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setWs(ws);
|
} catch (error) {
|
||||||
|
console.debug(new Date(), 'ws:error', error);
|
||||||
|
attemptReconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const attemptReconnect = () => {
|
||||||
|
retryCountRef.current += 1;
|
||||||
|
if (retryCountRef.current > MAX_RETRIES) {
|
||||||
|
console.debug(new Date(), 'ws:max_retries');
|
||||||
|
setError(true);
|
||||||
|
toast.error('Unable to connect to server after multiple attempts. Please refresh the page to try again.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backoffDelay = getBackoffDelay(retryCountRef.current);
|
||||||
|
console.debug(new Date(), `ws:retry attempt=${retryCountRef.current}/${MAX_RETRIES} delay=${backoffDelay}ms`);
|
||||||
|
|
||||||
|
if (reconnectTimeoutRef.current) {
|
||||||
|
clearTimeout(reconnectTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnectTimeoutRef.current = setTimeout(() => {
|
||||||
|
connectWs();
|
||||||
|
}, backoffDelay);
|
||||||
};
|
};
|
||||||
|
|
||||||
connectWs();
|
connectWs();
|
||||||
}
|
|
||||||
}, [ws, url, setIsWSReady, setError]);
|
|
||||||
|
|
||||||
return ws;
|
return () => {
|
||||||
|
isCleaningUpRef.current = true;
|
||||||
|
if (reconnectTimeoutRef.current) {
|
||||||
|
clearTimeout(reconnectTimeoutRef.current);
|
||||||
|
}
|
||||||
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
|
wsRef.current.close();
|
||||||
|
}
|
||||||
|
console.debug(new Date(), 'ws:cleanup');
|
||||||
|
};
|
||||||
|
}, [url, setIsWSReady, setError]);
|
||||||
|
|
||||||
|
return wsRef.current;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadMessages = async (
|
const loadMessages = async (
|
||||||
|
@ -291,7 +349,7 @@ const loadMessages = async (
|
||||||
return [msg.role, msg.content];
|
return [msg.role, msg.content];
|
||||||
}) as [string, string][];
|
}) as [string, string][];
|
||||||
|
|
||||||
console.log('[DEBUG] messages loaded');
|
console.debug(new Date(), 'app:messages_loaded');
|
||||||
|
|
||||||
document.title = messages[0].content;
|
document.title = messages[0].content;
|
||||||
|
|
||||||
|
@ -373,7 +431,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
||||||
return () => {
|
return () => {
|
||||||
if (ws?.readyState === 1) {
|
if (ws?.readyState === 1) {
|
||||||
ws.close();
|
ws.close();
|
||||||
console.log('[DEBUG] closed');
|
console.debug(new Date(), 'ws:cleanup');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
@ -388,12 +446,16 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMessagesLoaded && isWSReady) {
|
if (isMessagesLoaded && isWSReady) {
|
||||||
setIsReady(true);
|
setIsReady(true);
|
||||||
console.log('[DEBUG] ready');
|
console.debug(new Date(), 'app:ready');
|
||||||
}
|
}
|
||||||
}, [isMessagesLoaded, isWSReady]);
|
}, [isMessagesLoaded, isWSReady]);
|
||||||
|
|
||||||
const sendMessage = async (message: string, messageId?: string) => {
|
const sendMessage = async (message: string, messageId?: string) => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
|
toast.error('Cannot send message while disconnected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setMessageAppeared(false);
|
setMessageAppeared(false);
|
||||||
|
@ -404,7 +466,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
||||||
|
|
||||||
messageId = messageId ?? crypto.randomBytes(7).toString('hex');
|
messageId = messageId ?? crypto.randomBytes(7).toString('hex');
|
||||||
|
|
||||||
ws?.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: 'message',
|
type: 'message',
|
||||||
message: {
|
message: {
|
||||||
|
@ -558,7 +620,7 @@ const ChatWindow = ({ id }: { id?: string }) => {
|
||||||
|
|
||||||
return isReady ? (
|
return isReady ? (
|
||||||
notFound ? (
|
notFound ? (
|
||||||
<Error statusCode={404} />
|
<NextError statusCode={404} />
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{messages.length > 0 ? (
|
{messages.length > 0 ? (
|
||||||
|
|
Loading…
Add table
Reference in a new issue