ws reconnect
This commit is contained in:
parent
397a162385
commit
14f611e796
@ -29,9 +29,9 @@ export default function Page() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-row h-full">
|
<div className="flex flex-row h-full">
|
||||||
{/* Sidebar - hidden on screens smaller than lg (1024px) */}
|
{/* Sidebar - hidden on screens smaller than lg (1024px) */}
|
||||||
<div className="hidden lg:block">
|
{/* <div className="hidden lg:block">
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
</div>
|
</div> */}
|
||||||
<WSProvider>
|
<WSProvider>
|
||||||
<div className="flex-1 relative min-h-0">
|
<div className="flex-1 relative min-h-0">
|
||||||
<MapComponent />
|
<MapComponent />
|
||||||
|
|||||||
@ -3,21 +3,26 @@
|
|||||||
import React, { createContext, useContext, useRef, useState, ReactNode, useEffect } from 'react'
|
import React, { createContext, useContext, useRef, useState, ReactNode, useEffect } from 'react'
|
||||||
import { Map } from 'maplibre-gl';
|
import { Map } from 'maplibre-gl';
|
||||||
import { gql, useSubscription } from '@apollo/client';
|
import { gql, useSubscription } from '@apollo/client';
|
||||||
|
import { subscribeToConnectionState, reconnectWebSocket, WSConnectionState } from '@/lib/apollo-client';
|
||||||
|
import * as Sentry from '@sentry/nextjs';
|
||||||
|
|
||||||
// 定义MapContext的类型
|
// 定义WSContext的类型
|
||||||
interface WSContextType {
|
interface WSContextType {
|
||||||
wsStatus: WsStatus
|
wsStatus: WsStatus
|
||||||
setWsStatus: (status: WsStatus) => void
|
connectionState: WSConnectionState
|
||||||
data: any
|
data: any
|
||||||
loading: boolean
|
loading: boolean
|
||||||
error: any
|
error: any
|
||||||
restart: () => void
|
restart: () => void
|
||||||
|
forceReconnect: () => void
|
||||||
|
isOnline: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
enum WsStatus {
|
enum WsStatus {
|
||||||
CONNECTING = 'connecting',
|
CONNECTING = 'connecting',
|
||||||
CONNECTED = 'connected',
|
CONNECTED = 'connected',
|
||||||
DISCONNECTED = 'disconnected'
|
DISCONNECTED = 'disconnected',
|
||||||
|
RECONNECTING = 'reconnecting'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建Context
|
// 创建Context
|
||||||
@ -43,27 +48,101 @@ const SUBSCRIPTION_QUERY = gql`
|
|||||||
|
|
||||||
// Provider组件
|
// Provider组件
|
||||||
export function WSProvider({ children }: MapProviderProps) {
|
export function WSProvider({ children }: MapProviderProps) {
|
||||||
|
|
||||||
const [wsStatus, setWsStatus] = useState<WsStatus>(WsStatus.DISCONNECTED)
|
const [wsStatus, setWsStatus] = useState<WsStatus>(WsStatus.DISCONNECTED)
|
||||||
const { data, loading, error, restart } = useSubscription(SUBSCRIPTION_QUERY)
|
const [connectionState, setConnectionState] = useState<WSConnectionState>({
|
||||||
|
status: 'disconnected',
|
||||||
useEffect(() => {
|
reconnectAttempts: 0
|
||||||
if (loading) {
|
});
|
||||||
setWsStatus(WsStatus.CONNECTING)
|
const [isOnline, setIsOnline] = useState(typeof window !== 'undefined' ? navigator.onLine : true);
|
||||||
} else if (error) {
|
|
||||||
setWsStatus(WsStatus.DISCONNECTED)
|
const { data, loading, error, restart } = useSubscription(SUBSCRIPTION_QUERY, {
|
||||||
} else {
|
errorPolicy: 'all',
|
||||||
setWsStatus(WsStatus.CONNECTED)
|
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 = {
|
const value: WSContextType = {
|
||||||
wsStatus,
|
wsStatus,
|
||||||
setWsStatus,
|
connectionState,
|
||||||
data,
|
data,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
restart
|
restart,
|
||||||
|
forceReconnect,
|
||||||
|
isOnline
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -2,21 +2,119 @@ import { ApolloClient, InMemoryCache, createHttpLink, from, split } from '@apoll
|
|||||||
import { getMainDefinition } from '@apollo/client/utilities';
|
import { getMainDefinition } from '@apollo/client/utilities';
|
||||||
import { setContext } from '@apollo/client/link/context';
|
import { setContext } from '@apollo/client/link/context';
|
||||||
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
|
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';
|
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({
|
const httpLink = createHttpLink({
|
||||||
uri: process.env.NEXT_PUBLIC_GRAPHQL_BACKEND_URL || 'http://localhost:3050/graphql',
|
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({
|
let wsClient: Client | null = null;
|
||||||
// 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',
|
const createWSClient = () => {
|
||||||
// url: "ws://45.152.65.37:3050/ws"
|
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 }) => {
|
const authLink = setContext((_, { headers }) => {
|
||||||
// 从 localStorage 获取 token
|
// 从 localStorage 获取 token
|
||||||
@ -54,4 +152,13 @@ export const createApolloClient = () => {
|
|||||||
link,
|
link,
|
||||||
cache: new InMemoryCache(),
|
cache: new InMemoryCache(),
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reconnectWebSocket = () => {
|
||||||
|
if (wsClient) {
|
||||||
|
wsClient.dispose();
|
||||||
|
wsClient = null;
|
||||||
|
}
|
||||||
|
wsClient = createWSClient();
|
||||||
|
return wsClient;
|
||||||
};
|
};
|
||||||
Loading…
Reference in New Issue
Block a user