memowake-front/lib/websocket-util.ts

335 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Constants from 'expo-constants';
import * as SecureStore from 'expo-secure-store';
import { TFunction } from 'i18next';
import { Platform } from 'react-native';
// 从环境变量或默认值中定义 WebSocket 端点
export const WEBSOCKET_ENDPOINT = process.env.EXPO_PUBLIC_WEBSOCKET_ENDPOINT || Constants.expoConfig?.extra?.WEBSOCKET_ENDPOINT;
export type WebSocketStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
type StatusListener = (status: WebSocketStatus) => void;
// 消息监听器类型
type MessageListener = (data: any) => void;
// 根据后端 Rust 定义的 WsMessage 枚举创建 TypeScript 类型
export type WsMessage =
| { type: 'Chat', session_id: string, message: string, image_material_ids?: string[], video_material_ids?: string[] }
| { type: 'ChatResponse', session_id: string, message: any, message_id?: string }
| { type: 'ChatStream', session_id: string, chunk: string }
| { type: 'ChatStreamEnd', session_id: string, message: any }
| { type: 'Error', code: string, message: string }
| { type: 'Ping' }
| { type: 'Pong' }
| { type: 'Connected', user_id: number };
class WebSocketManager {
private ws: WebSocket | null = null;
private status: WebSocketStatus = 'disconnected';
private messageListeners: Map<string, Set<(message: WsMessage) => void>> = new Map();
private statusListeners: Set<StatusListener> = new Set();
private reconnectAttempts = 0;
private readonly maxReconnectAttempts = 1;
private readonly reconnectInterval = 1000; // 初始重连间隔为1秒
private pingIntervalId: ReturnType<typeof setInterval> | null = null;
private readonly pingInterval = 30000; // 30秒发送一次心跳
constructor() {
// 这是一个单例类,连接通过调用 connect() 方法来启动
}
/**
* 获取当前 WebSocket 连接状态。
*/
public getStatus(): WebSocketStatus {
return this.status;
}
/**
* 启动 WebSocket 连接。
* 会自动获取并使用存储的认证 token。
*/
public async connect() {
// 防止重复连接
if (this.ws && (this.status === 'connected' || this.status === 'connecting')) {
return;
}
this.setStatus('connecting');
let token = "";
try {
if (Platform.OS === 'web') {
token = localStorage.getItem('token') || "";
} else {
token = await SecureStore.getItemAsync('token') || "";
}
} catch (error) {
console.error('获取认证 token 时出错:', error);
this.setStatus('disconnected');
return;
}
if (!token) {
console.error('WebSocket: 未找到认证 token无法连接。');
this.setStatus('disconnected');
return;
} else {
console.log('WebSocket: 认证 token:', token);
}
// 检查 WebSocket 端点是否已定义
if (!WEBSOCKET_ENDPOINT) {
console.error('WebSocket: 未定义端点 URL。');
this.setStatus('disconnected');
return;
}
const url = `${WEBSOCKET_ENDPOINT}?token=${token}`;
console.log('WebSocket: 连接 URL:', url);
try {
this.ws = new WebSocket(url);
} catch (error) {
console.error('创建 WebSocket 连接时出错:', error);
this.setStatus('disconnected');
return;
}
this.ws.onopen = () => {
console.log('WebSocket connected');
this.setStatus('connected');
this.reconnectAttempts = 0; // 重置重连尝试次数
this.startPing();
};
this.ws.onmessage = (event) => {
try {
// 检查事件数据是否存在
if (!event.data) {
console.warn('WebSocket received empty message');
return;
}
const message: WsMessage = JSON.parse(event.data);
// console.log('WebSocket received message:', message)
// 根据消息类型分发
const eventListeners = this.messageListeners.get(message.type);
if (eventListeners) {
eventListeners.forEach(callback => {
try {
callback(message);
} catch (error) {
console.error(`处理消息类型 ${message.type} 时出错:`, error);
}
});
}
// 可以在这里处理通用的消息,比如 Pong
if (message.type === 'Pong') {
// console.log('Received Pong');
}
} catch (error) {
console.error('处理 WebSocket 消息失败:', error);
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.setStatus('disconnected');
this.handleReconnect();
};
this.ws.onclose = () => {
console.log('WebSocket disconnected');
this.ws = null;
this.stopPing();
// 只有在不是手动断开连接时才重连
if (this.status !== 'disconnected') {
this.setStatus('reconnecting');
this.handleReconnect();
}
};
}
/**
* 处理自动重连逻辑,使用指数退避策略。
*/
private handleReconnect() {
try {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1);
console.log(`${delay / 1000}秒后尝试重新连接 (第 ${this.reconnectAttempts} 次)...`);
setTimeout(() => {
this.connect();
}, delay);
} else {
console.error('WebSocket 重连失败,已达到最大尝试次数。');
this.setStatus('disconnected');
}
} catch (error) {
console.error('处理 WebSocket 重连时出错:', error);
this.setStatus('disconnected');
}
}
/**
* 发送消息到 WebSocket 服务器。
* @param message 要发送的消息对象,必须包含 type 字段。
*/
public send(message: WsMessage) {
if (this.status !== 'connected' || !this.ws) {
console.error('WebSocket 未连接,无法发送消息。');
return;
}
try {
const messageString = JSON.stringify(message);
this.ws.send(messageString);
} catch (error) {
console.error('发送 WebSocket 消息时出错:', error);
}
}
/**
* 订阅指定消息类型的消息。
* @param type 消息类型,例如 'ChatResponse'。
* @param callback 收到消息时的回调函数。
*/
public subscribe(type: WsMessage['type'], callback: (message: WsMessage) => void) {
try {
if (!this.messageListeners.has(type)) {
this.messageListeners.set(type, new Set());
}
this.messageListeners.get(type)?.add(callback);
} catch (error) {
console.error(`订阅消息类型 ${type} 时出错:`, error);
}
}
/**
* 取消订阅指定消息类型的消息。
* @param type 消息类型。
* @param callback 要移除的回调函数。
*/
public unsubscribe(type: WsMessage['type'], callback: (message: WsMessage) => void) {
try {
const eventListeners = this.messageListeners.get(type);
if (eventListeners) {
eventListeners.delete(callback);
if (eventListeners.size === 0) {
this.messageListeners.delete(type);
}
}
} catch (error) {
console.error(`取消订阅消息类型 ${type} 时出错:`, error);
}
}
/**
* 手动断开 WebSocket 连接。
*/
public disconnect() {
try {
this.setStatus('disconnected');
if (this.ws) {
this.ws.close();
}
} catch (error) {
console.error('断开 WebSocket 连接时出错:', error);
} finally {
this.stopPing();
}
}
private setStatus(status: WebSocketStatus) {
try {
if (this.status !== status) {
this.status = status;
this.statusListeners.forEach(listener => {
try {
listener(status);
} catch (error) {
console.error('调用状态监听器时出错:', error);
}
});
}
} catch (error) {
console.error('设置 WebSocket 状态时出错:', error);
}
}
public subscribeStatus(listener: StatusListener) {
try {
this.statusListeners.add(listener);
// Immediately invoke with current status
try {
listener(this.status);
} catch (error) {
console.error('调用状态监听器时出错:', error);
}
} catch (error) {
console.error('订阅状态监听器时出错:', error);
}
}
public unsubscribeStatus(listener: StatusListener) {
try {
this.statusListeners.delete(listener);
} catch (error) {
console.error('取消订阅状态监听器时出错:', error);
}
}
/**
* 启动心跳机制。
*/
private startPing() {
try {
this.stopPing(); // 先停止任何可能正在运行的计时器
this.pingIntervalId = setInterval(() => {
this.send({ type: 'Ping' });
}, this.pingInterval);
} catch (error) {
console.error('启动心跳机制时出错:', error);
}
}
/**
* 停止心跳机制。
*/
private stopPing() {
try {
if (this.pingIntervalId) {
clearInterval(this.pingIntervalId);
this.pingIntervalId = null;
}
} catch (error) {
console.error('停止心跳机制时出错:', error);
}
}
}
// 导出一个单例,确保整个应用共享同一个 WebSocket 连接
let webSocketManagerInstance: WebSocketManager | null = null;
export const getWebSocketManager = (): WebSocketManager => {
if (!webSocketManagerInstance) {
webSocketManagerInstance = new WebSocketManager();
}
return webSocketManagerInstance;
};
// webscoket 错误映射
export const getWebSocketErrorMessage = (key: string, t: TFunction) => {
const messages = {
'INSUFFICIENT_POINTS': t('ask:ask.insufficientPoints'),
'INVALID_TOKEN': t('ask:ask.invalidToken'),
'NOT_FOUND': t('ask:ask.notFound'),
'PERMISSION_DENIED': t('ask:ask.permissionDenied'),
'NOT_CONNECTED': t('ask:ask.notConnected'),
};
return messages[key as keyof typeof messages] || t('ask:ask.unknownError');
};