ws reconnect
This commit is contained in:
parent
397a162385
commit
14f611e796
@ -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 />
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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;
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user