ws reconnect

This commit is contained in:
Tsuki 2025-08-24 23:56:56 +08:00
parent 397a162385
commit 14f611e796
3 changed files with 211 additions and 25 deletions

View File

@ -29,9 +29,9 @@ export default function Page() {
return (
<div className="flex flex-row h-full">
{/* Sidebar - hidden on screens smaller than lg (1024px) */}
<div className="hidden lg:block">
{/* <div className="hidden lg:block">
<AppSidebar />
</div>
</div> */}
<WSProvider>
<div className="flex-1 relative min-h-0">
<MapComponent />

View File

@ -3,21 +3,26 @@
import React, { createContext, useContext, useRef, useState, ReactNode, useEffect } from 'react'
import { Map } from 'maplibre-gl';
import { gql, useSubscription } from '@apollo/client';
import { subscribeToConnectionState, reconnectWebSocket, WSConnectionState } from '@/lib/apollo-client';
import * as Sentry from '@sentry/nextjs';
// 定义MapContext的类型
// 定义WSContext的类型
interface WSContextType {
wsStatus: WsStatus
setWsStatus: (status: WsStatus) => void
connectionState: WSConnectionState
data: any
loading: boolean
error: any
restart: () => void
forceReconnect: () => void
isOnline: boolean
}
enum WsStatus {
CONNECTING = 'connecting',
CONNECTED = 'connected',
DISCONNECTED = 'disconnected'
DISCONNECTED = 'disconnected',
RECONNECTING = 'reconnecting'
}
// 创建Context
@ -43,27 +48,101 @@ const SUBSCRIPTION_QUERY = gql`
// Provider组件
export function WSProvider({ children }: MapProviderProps) {
const [wsStatus, setWsStatus] = useState<WsStatus>(WsStatus.DISCONNECTED)
const { data, loading, error, restart } = useSubscription(SUBSCRIPTION_QUERY)
const [connectionState, setConnectionState] = useState<WSConnectionState>({
status: 'disconnected',
reconnectAttempts: 0
});
const [isOnline, setIsOnline] = useState(typeof window !== 'undefined' ? navigator.onLine : true);
useEffect(() => {
if (loading) {
setWsStatus(WsStatus.CONNECTING)
} else if (error) {
setWsStatus(WsStatus.DISCONNECTED)
} else {
setWsStatus(WsStatus.CONNECTED)
const { data, loading, error, restart } = useSubscription(SUBSCRIPTION_QUERY, {
errorPolicy: 'all',
onError: (error) => {
Sentry.captureException(error);
}
}, [data])
});
// 监听网络状态变化
useEffect(() => {
if (typeof window === 'undefined') return;
const handleOnline = () => {
setIsOnline(true);
if (wsStatus === WsStatus.DISCONNECTED) {
restart();
}
};
const handleOffline = () => {
setIsOnline(false);
setWsStatus(WsStatus.DISCONNECTED);
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, [wsStatus, restart]);
// 监听 WebSocket 连接状态
useEffect(() => {
const unsubscribe = subscribeToConnectionState((state) => {
setConnectionState(state);
switch (state.status) {
case 'connecting':
setWsStatus(WsStatus.CONNECTING);
break;
case 'connected':
setWsStatus(WsStatus.CONNECTED);
break;
case 'disconnected':
setWsStatus(WsStatus.DISCONNECTED);
break;
case 'reconnecting':
setWsStatus(WsStatus.RECONNECTING);
break;
}
});
return unsubscribe;
}, []);
// 监听订阅状态变化
useEffect(() => {
if (loading && wsStatus !== WsStatus.CONNECTING) {
setWsStatus(WsStatus.CONNECTING);
} else if (error && !loading) {
if (isOnline) {
setTimeout(() => {
restart();
}, 5000);
}
}
}, [loading, error, isOnline, restart, wsStatus]);
const forceReconnect = () => {
Sentry.startSpan({
op: 'websocket.manual-reconnect',
name: 'Manual WebSocket Reconnection'
}, () => {
reconnectWebSocket();
restart();
});
};
const value: WSContextType = {
wsStatus,
setWsStatus,
connectionState,
data,
loading,
error,
restart
restart,
forceReconnect,
isOnline
}
return (

View File

@ -2,21 +2,119 @@ import { ApolloClient, InMemoryCache, createHttpLink, from, split } from '@apoll
import { getMainDefinition } from '@apollo/client/utilities';
import { setContext } from '@apollo/client/link/context';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { createClient, Client } from 'graphql-ws';
import * as Sentry from '@sentry/nextjs';
const TOKEN_KEY = 'auth_token';
export interface WSConnectionState {
status: 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
error?: Error;
reconnectAttempts: number;
}
let wsConnectionState: WSConnectionState = {
status: 'disconnected',
reconnectAttempts: 0
};
const connectionStateListeners: Set<(state: WSConnectionState) => void> = new Set();
export const subscribeToConnectionState = (listener: (state: WSConnectionState) => void) => {
connectionStateListeners.add(listener);
listener(wsConnectionState);
return () => {
connectionStateListeners.delete(listener);
};
};
const updateConnectionState = (update: Partial<WSConnectionState>) => {
wsConnectionState = { ...wsConnectionState, ...update };
connectionStateListeners.forEach(listener => listener(wsConnectionState));
};
const httpLink = createHttpLink({
uri: process.env.NEXT_PUBLIC_GRAPHQL_BACKEND_URL || 'http://localhost:3050/graphql',
// uri: "http://45.152.65.37:3050/graphql"
});
const wsLink = new GraphQLWsLink(createClient({
// url: "ws://127.0.0.1:3050/ws",
url: process.env.NEXT_PUBLIC_GRAPHQL_BACKEND_URL?.replace('/graphql', '/ws')?.replace('http://', 'ws://') || 'ws://localhost:3050/ws',
// url: "ws://45.152.65.37:3050/ws"
}));
let wsClient: Client | null = null;
const createWSClient = () => {
const wsUrl = process.env.NEXT_PUBLIC_GRAPHQL_BACKEND_URL?.replace('/graphql', '/ws')?.replace('http://', 'ws://') || 'ws://localhost:3050/ws';
return createClient({
url: wsUrl,
connectionParams: () => {
const token = typeof window !== 'undefined' ? localStorage.getItem(TOKEN_KEY) : null;
return token ? { authorization: `Bearer ${token}` } : {};
},
retryAttempts: 5,
retryWait: async function (attempt) {
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
updateConnectionState({
status: 'reconnecting',
reconnectAttempts: attempt + 1
});
Sentry.startSpan({
op: 'websocket.reconnect',
name: 'WebSocket Reconnection Attempt'
}, (span) => {
span.setAttribute('attempt', attempt + 1);
span.setAttribute('delay', delay);
});
await new Promise(resolve => setTimeout(resolve, delay));
},
on: {
connecting: () => {
updateConnectionState({ status: 'connecting' });
},
opened: () => {
updateConnectionState({
status: 'connected',
error: undefined,
reconnectAttempts: 0
});
Sentry.startSpan({
op: 'websocket.connect',
name: 'WebSocket Connection Established'
}, () => { });
},
closed: (event: any) => {
const error = event.reason ? new Error(event.reason) : undefined;
updateConnectionState({
status: 'disconnected',
error
});
if (error) {
Sentry.captureException(error);
}
},
error: (error) => {
updateConnectionState({
status: 'disconnected',
error: error instanceof Error ? error : new Error('WebSocket error')
});
Sentry.captureException(error);
}
}
});
};
const getOrCreateWSClient = () => {
if (!wsClient) {
wsClient = createWSClient();
}
return wsClient;
};
const wsLink = new GraphQLWsLink(getOrCreateWSClient());
const authLink = setContext((_, { headers }) => {
// 从 localStorage 获取 token
@ -55,3 +153,12 @@ export const createApolloClient = () => {
cache: new InMemoryCache(),
});
};
export const reconnectWebSocket = () => {
if (wsClient) {
wsClient.dispose();
wsClient = null;
}
wsClient = createWSClient();
return wsClient;
};