diff --git a/app/ws-context.tsx b/app/ws-context.tsx
index 89de075..bf34965 100644
--- a/app/ws-context.tsx
+++ b/app/ws-context.tsx
@@ -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.DISCONNECTED)
- const { data, loading, error, restart } = useSubscription(SUBSCRIPTION_QUERY)
-
- useEffect(() => {
- if (loading) {
- setWsStatus(WsStatus.CONNECTING)
- } else if (error) {
- setWsStatus(WsStatus.DISCONNECTED)
- } else {
- setWsStatus(WsStatus.CONNECTED)
+ const [connectionState, setConnectionState] = useState({
+ status: 'disconnected',
+ reconnectAttempts: 0
+ });
+ const [isOnline, setIsOnline] = useState(typeof window !== 'undefined' ? navigator.onLine : true);
+
+ 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 (
diff --git a/lib/apollo-client.ts b/lib/apollo-client.ts
index 780657e..8e1b9dc 100644
--- a/lib/apollo-client.ts
+++ b/lib/apollo-client.ts
@@ -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, ...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
@@ -54,4 +152,13 @@ export const createApolloClient = () => {
link,
cache: new InMemoryCache(),
});
+};
+
+export const reconnectWebSocket = () => {
+ if (wsClient) {
+ wsClient.dispose();
+ wsClient = null;
+ }
+ wsClient = createWSClient();
+ return wsClient;
};
\ No newline at end of file