Files
frontend/src/services/wsService.ts
lafay 5614b4078a
All checks were successful
Frontend CI / ota-android (push) Successful in 13m7s
Frontend CI / build-and-push-web (push) Successful in 16m58s
Frontend CI / build-android-apk (push) Successful in 1h4m18s
feat(profile): add chat settings and language options to profile settings
- Introduced hrefProfileChatSettings() for navigation to chat settings.
- Updated SettingsScreen to include new settings items for chat settings, language selection, data storage, terms, and privacy policy.
- Enhanced user experience by providing alerts for language and data storage options.

This update improves the profile settings interface by adding more customization options for users.
2026-04-01 02:17:36 +08:00

1163 lines
30 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 { AppState, AppStateStatus } from 'react-native';
import { api, WS_URL } from './api';
import { MessageCategory, SystemMessageType, SystemMessageExtraData, MessageSegment } from '../types/dto';
import { systemNotificationService } from './systemNotificationService';
export type WSMessageType =
| 'chat'
| 'message'
| 'read'
| 'typing'
| 'recall'
| 'notification'
| 'announcement'
| 'group_message'
| 'group_typing'
| 'group_notice'
| 'group_mention'
| 'group_read'
| 'group_recall'
| 'notice'
| 'request'
| 'meta'
| 'private'
| 'group'
| 'follow'
| 'like'
| 'comment'
| 'heartbeat'
| 'pong'
| 'message_sent'
| 'message_recalled'
| 'error'
| 'call_incoming'
| 'call_accepted'
| 'call_rejected'
| 'call_busy'
| 'call_sdp'
| 'call_ice'
| 'call_ended'
| 'call_peer_muted'
| 'call_invited'
| 'call_answered_elsewhere';
export interface WSCallIncomingMessage {
type: 'call_incoming';
call_id: string;
conversation_id: string;
caller_id: string;
call_type: string;
media_type?: string;
created_at: number;
lifetime?: number;
ice_servers?: ICEServerConfig[];
}
export interface ICEServerConfig {
urls: string[];
username?: string;
credential?: string;
}
export interface WSCallAcceptedMessage {
type: 'call_accepted';
call_id: string;
started_at: number;
ice_servers?: ICEServerConfig[];
}
export interface WSCallRejectedMessage {
type: 'call_rejected';
call_id: string;
reason?: string;
}
export interface WSCallBusyMessage {
type: 'call_busy';
call_id: string;
}
export interface WSCallEndedMessage {
type: 'call_ended';
call_id: string;
ended_by?: string;
reason?: string;
duration?: number;
ended_at?: number;
}
export interface WSCallSDPMessage {
type: 'call_sdp';
call_id: string;
from_id: string;
payload: {
sdp_type: string;
sdp: string;
};
}
export interface WSCallICEMessage {
type: 'call_ice';
call_id: string;
from_id: string;
payload: {
candidate: string;
};
}
export interface WSCallPeerMutedMessage {
type: 'call_peer_muted';
call_id: string;
user_id: string;
muted: boolean;
}
export interface WSCallInvitedMessage {
type: 'call_invited';
call_id: string;
conversation_id: string;
callee_id: string;
}
export interface WSCallAnsweredElsewhereMessage {
type: 'call_answered_elsewhere';
call_id: string;
reason?: string;
}
export interface WSErrorMessage {
type: 'error';
code: string;
message: string;
}
export interface WSChatMessage {
type: 'chat';
conversation_id: string;
id: string;
sender_id: string;
seq: number;
segments?: MessageSegment[];
created_at: string;
}
export interface WSReadMessage {
type: 'read';
conversation_id: string;
user_id: string;
seq: number;
}
export interface WSTypingMessage {
type: 'typing';
conversation_id: string;
user_id: string;
is_typing: boolean;
}
export interface WSRecallMessage {
type: 'recall';
conversation_id: string;
message_id: string;
}
export interface WSNotificationMessage {
type: 'notification';
id: string;
sender_id?: string;
receiver_id?: string;
content: string;
category?: MessageCategory;
system_type?: SystemMessageType;
extra_data?: SystemMessageExtraData;
created_at: string;
}
export interface WSAnnouncementMessage {
type: 'announcement';
id: string;
sender_id?: string;
receiver_id?: string;
content: string;
category?: MessageCategory;
system_type?: SystemMessageType;
extra_data?: SystemMessageExtraData;
created_at: string;
}
export interface WSGroupMentionMessage {
type: 'group_mention';
group_id: string;
conversation_id: string;
message_id: string;
from_user_id: string;
content: string;
mention_all: boolean;
created_at: string;
}
export interface WSGroupChatMessage {
type: 'group_message';
conversation_id: string;
group_id: string;
id: string;
sender_id: string;
seq: number;
segments?: MessageSegment[];
created_at: string;
}
export interface WSGroupTypingMessage {
type: 'group_typing';
group_id: string;
user_id: string;
is_typing: boolean;
}
export type GroupNoticeType = 'member_join' | 'member_leave' | 'member_removed' | 'role_changed' | 'muted' | 'unmuted';
export interface WSGroupNoticeMessage {
type: 'group_notice';
notice_type: GroupNoticeType;
group_id: string;
data: {
user_id?: string;
operator_id?: string;
role?: string;
[key: string]: any;
};
timestamp: number;
message_id?: string;
seq?: number;
}
export interface WSGroupReadMessage {
type: 'group_read';
group_id: string;
conversation_id: string;
user_id: string;
seq: number;
}
export interface WSGroupRecallMessage {
type: 'group_recall';
group_id: string;
conversation_id: string;
message_id: string;
}
export interface WSServerMessage {
event_id?: number;
type: string;
ts?: number;
payload?: any;
}
export type WSMessage =
| WSChatMessage
| WSReadMessage
| WSTypingMessage
| WSRecallMessage
| WSNotificationMessage
| WSAnnouncementMessage
| WSGroupChatMessage
| WSGroupTypingMessage
| WSGroupNoticeMessage
| WSGroupMentionMessage
| WSGroupReadMessage
| WSGroupRecallMessage
| WSCallIncomingMessage
| WSCallAcceptedMessage
| WSCallRejectedMessage
| WSCallBusyMessage
| WSCallEndedMessage
| WSCallSDPMessage
| WSCallICEMessage
| WSCallPeerMutedMessage
| WSCallInvitedMessage
| WSCallAnsweredElsewhereMessage
| WSErrorMessage;
type MessageHandler<T extends WSMessage = WSMessage> = (message: T) => void;
type ConnectionHandler = () => void;
// 发送队列中的消息
interface PendingMessage {
id: string;
type: string;
payload: any;
resolve: (value: any) => void;
reject: (reason: any) => void;
timestamp: number;
}
class WebSocketService {
private ws: WebSocket | null = null;
private isConnecting = false;
private connected = false;
private reconnectAttempts = 0;
private maxReconnectAttempts = 20;
private reconnectDelay = 3000;
private reconnectTimer: NodeJS.Timeout | null = null;
private heartbeatTimer: NodeJS.Timeout | null = null;
private heartbeatInterval = 30000; // 30秒心跳
private messageHandlers: Map<WSMessageType, MessageHandler[]> = new Map();
private connectionHandlers: ConnectionHandler[] = [];
private disconnectionHandlers: ConnectionHandler[] = [];
private appStateSubscription: any = null;
private lastAppState: AppStateStatus = 'active';
private shouldRun = false;
private lastEventId = '';
private lastActivityAt = 0;
// 发送队列
private pendingMessages: PendingMessage[] = [];
private messageIdCounter = 0;
// 降级状态
private isFallbackMode = false;
private fallbackCheckTimer: NodeJS.Timeout | null = null;
private getWSUrl(token: string | null): string {
return `${WS_URL}?token=${encodeURIComponent(token || '')}`;
}
async connect(): Promise<boolean> {
this.shouldRun = true;
if (this.isConnecting || this.isConnected()) {
console.log('[WebSocket] Already connected or connecting');
return true;
}
this.isConnecting = true;
console.log('[WebSocket] Connecting...');
try {
const token = await api.getToken();
if (!token) {
console.error('[WebSocket] No token available');
this.isConnecting = false;
this.scheduleReconnect();
return false;
}
const url = this.getWSUrl(token);
console.log('[WebSocket] Connecting to:', url.replace(token, '***'));
this.ws = new WebSocket(url);
this.ws.onopen = () => {
console.log('[WebSocket] Connected successfully');
this.isConnecting = false;
this.connected = true;
this.reconnectAttempts = 0;
this.isFallbackMode = false;
this.markActivity();
this.startHeartbeat();
this.connectionHandlers.forEach(h => h());
// 发送队列中的消息
this.flushPendingMessages();
};
this.ws.onmessage = (event) => {
this.markActivity();
this.handleIncoming(event.data);
};
this.ws.onerror = (error) => {
console.error('[WebSocket] Error:', error);
};
this.ws.onclose = (event) => {
console.log('[WebSocket] Closed:', event.code, event.reason);
this.handleDisconnected();
};
return true;
} catch (error) {
console.error('[WebSocket] Connection error:', error);
this.isConnecting = false;
this.scheduleReconnect();
return false;
}
}
private handleIncoming(data: string): void {
// 后端 writePump 会将多条消息以 \n 分隔合并发送,需要逐条解析
const lines = data.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
const msg: WSServerMessage = JSON.parse(trimmed);
this.processMessage(msg);
} catch (error) {
console.error('Failed to parse WebSocket message:', error, 'raw:', trimmed);
}
}
}
private processMessage(msg: WSServerMessage): void {
// 处理pong响应
if (msg.type === 'pong') {
return;
}
// 处理错误消息 - emit 到上层处理
if (msg.type === 'error') {
console.error('WebSocket error:', msg.payload);
const err: WSErrorMessage = {
type: 'error',
code: msg.payload?.code || 'unknown',
message: msg.payload?.message || 'Unknown error',
};
this.emit('error', err);
return;
}
// 处理消息发送成功响应
if (msg.type === 'message_sent') {
this.handleMessageSent(msg.payload);
return;
}
// 处理消息撤回响应
if (msg.type === 'message_recalled') {
this.handleMessageRecalled(msg.payload);
return;
}
// 保存事件ID用于断线重连
if (msg.event_id) {
this.lastEventId = String(msg.event_id);
}
// 分发事件
this.dispatchServerMessage(msg);
}
private dispatchServerMessage(msg: WSServerMessage): void {
const { type, payload } = msg;
switch (type) {
case 'chat_message':
this.handleChatMessage(payload);
break;
case 'message_read':
this.handleMessageRead(payload);
break;
case 'typing':
this.handleTyping(payload);
break;
case 'message_recall':
this.handleMessageRecall(payload);
break;
case 'group_notice':
this.handleGroupNotice(payload);
break;
case 'system_notification':
this.handleSystemNotification(payload);
break;
// Call signaling messages
case 'call_incoming':
this.handleCallIncoming(payload);
break;
case 'call_accepted':
this.handleCallAccepted(payload);
break;
case 'call_rejected':
this.handleCallRejected(payload);
break;
case 'call_busy':
this.handleCallBusy(payload);
break;
case 'call_sdp':
this.handleCallSDP(payload);
break;
case 'call_ice':
this.handleCallICE(payload);
break;
case 'call_ended':
this.handleCallEnded(payload);
break;
case 'call_peer_muted':
this.handleCallPeerMuted(payload);
break;
case 'call_invited':
this.handleCallInvited(payload);
break;
case 'call_answered_elsewhere':
this.handleCallAnsweredElsewhere(payload);
break;
default:
console.log('Unknown message type:', type);
}
}
private handleChatMessage(payload: any): void {
const detailType = payload?.detail_type || 'private';
const m = payload?.message || payload;
if (detailType === 'group') {
const gm: WSGroupChatMessage = {
type: 'group_message',
conversation_id: m.conversation_id,
group_id: String(m.group_id ?? ''),
id: m.id,
sender_id: m.sender_id,
seq: Number(m.seq || 0),
segments: m.segments || [],
created_at: m.created_at || new Date().toISOString(),
};
this.emit('group_message', gm);
} else {
const cm: WSChatMessage = {
type: 'chat',
conversation_id: m.conversation_id,
id: m.id,
sender_id: m.sender_id,
seq: Number(m.seq || 0),
segments: m.segments || [],
created_at: m.created_at || new Date().toISOString(),
};
this.emit('chat', cm);
}
}
private handleMessageRead(payload: any): void {
const detailType = payload?.detail_type || 'private';
if (detailType === 'group') {
const m: WSGroupReadMessage = {
type: 'group_read',
group_id: String(payload.group_id ?? ''),
conversation_id: payload.conversation_id,
user_id: payload.user_id,
seq: Number(payload.seq || 0),
};
this.emit('group_read', m);
} else {
const m: WSReadMessage = {
type: 'read',
conversation_id: payload.conversation_id,
user_id: payload.user_id,
seq: Number(payload.seq || 0),
};
this.emit('read', m);
}
}
private handleTyping(payload: any): void {
const detailType = payload?.detail_type || 'private';
if (detailType === 'group') {
const m: WSGroupTypingMessage = {
type: 'group_typing',
group_id: String(payload.group_id ?? ''),
user_id: payload.user_id,
is_typing: payload.is_typing !== false,
};
this.emit('group_typing', m);
} else {
const m: WSTypingMessage = {
type: 'typing',
conversation_id: payload.conversation_id,
user_id: payload.user_id,
is_typing: payload.is_typing !== false,
};
this.emit('typing', m);
}
}
private handleMessageRecall(payload: any): void {
const detailType = payload?.detail_type || 'private';
if (detailType === 'group') {
const m: WSGroupRecallMessage = {
type: 'group_recall',
group_id: String(payload.group_id ?? ''),
conversation_id: payload.conversation_id,
message_id: payload.message_id,
};
this.emit('group_recall', m);
} else {
const m: WSRecallMessage = {
type: 'recall',
conversation_id: payload.conversation_id,
message_id: payload.message_id,
};
this.emit('recall', m);
}
}
private handleGroupNotice(payload: any): void {
const m: WSGroupNoticeMessage = {
type: 'group_notice',
notice_type: payload.notice_type,
group_id: String(payload.group_id ?? ''),
data: payload.data || {},
timestamp: payload.timestamp || Date.now(),
message_id: payload.message_id,
seq: payload.seq,
};
this.emit('group_notice', m);
}
private handleSystemNotification(payload: any): void {
const m: WSNotificationMessage = {
type: 'notification',
id: String(payload.id ?? ''),
content: payload.content || '',
created_at: payload.created_at || new Date().toISOString(),
};
this.emit('notification', m);
systemNotificationService.handleWSMessage(m as any).catch(() => {});
}
// Call signaling handlers
private handleCallIncoming(payload: any): void {
console.log('[WSService] call_incoming payload:', JSON.stringify({
call_type: payload.call_type,
media_type: payload.media_type,
}));
const m: WSCallIncomingMessage = {
type: 'call_incoming',
call_id: payload.call_id,
conversation_id: payload.conversation_id,
caller_id: payload.caller_id,
call_type: payload.media_type || payload.call_type,
created_at: payload.created_at,
lifetime: payload.lifetime,
ice_servers: payload.ice_servers,
};
this.emit('call_incoming', m);
}
private handleCallAccepted(payload: any): void {
const m: WSCallAcceptedMessage = {
type: 'call_accepted',
call_id: payload.call_id,
started_at: payload.started_at,
ice_servers: payload.ice_servers,
};
this.emit('call_accepted', m);
}
private handleCallRejected(payload: any): void {
const m: WSCallRejectedMessage = {
type: 'call_rejected',
call_id: payload.call_id,
reason: payload.reason,
};
this.emit('call_rejected', m);
}
private handleCallBusy(payload: any): void {
const m: WSCallBusyMessage = {
type: 'call_busy',
call_id: payload.call_id,
};
this.emit('call_busy', m);
}
private handleCallSDP(payload: any): void {
const m: WSCallSDPMessage = {
type: 'call_sdp',
call_id: payload.call_id,
from_id: payload.from_id,
payload: payload.payload,
};
this.emit('call_sdp', m);
}
private handleCallICE(payload: any): void {
const m: WSCallICEMessage = {
type: 'call_ice',
call_id: payload.call_id,
from_id: payload.from_id,
payload: payload.payload,
};
this.emit('call_ice', m);
}
private handleCallEnded(payload: any): void {
const m: WSCallEndedMessage = {
type: 'call_ended',
call_id: payload.call_id,
ended_by: payload.ended_by,
reason: payload.reason,
duration: payload.duration,
ended_at: payload.ended_at,
};
this.emit('call_ended', m);
}
private handleCallPeerMuted(payload: any): void {
const m: WSCallPeerMutedMessage = {
type: 'call_peer_muted',
call_id: payload.call_id,
user_id: payload.user_id,
muted: payload.muted,
};
this.emit('call_peer_muted', m);
}
private handleCallInvited(payload: any): void {
const m: WSCallInvitedMessage = {
type: 'call_invited',
call_id: payload.call_id,
conversation_id: payload.conversation_id,
callee_id: payload.callee_id,
};
this.emit('call_invited', m);
}
private handleCallAnsweredElsewhere(payload: any): void {
const m: WSCallAnsweredElsewhereMessage = {
type: 'call_answered_elsewhere',
call_id: payload.call_id,
reason: payload.reason,
};
this.emit('call_answered_elsewhere', m);
}
// Call signaling send methods
sendCallInvite(conversationId: string, calleeId: string, callType: 'voice' | 'video' = 'voice'): void {
this.sendFireAndForget('call_invite', {
conversation_id: conversationId,
callee_id: calleeId,
call_type: callType,
});
}
/**
* 查询当前活跃的通话
* 用于 WebSocket 重连后恢复通话状态
*/
async getActiveCall(): Promise<any | null> {
try {
const { api } = await import('./api');
const response = await api.get('/calls/active');
return response.data || null;
} catch (error) {
console.log('[WSService] No active call found');
return null;
}
}
sendCallAnswer(callId: string): void {
this.sendFireAndForget('call_answer', { call_id: callId });
}
sendCallReject(callId: string): void {
this.sendFireAndForget('call_reject', { call_id: callId });
}
sendCallBusy(callId: string): void {
this.sendFireAndForget('call_busy', { call_id: callId });
}
sendCallSDP(callId: string, sdpType: string, sdp: string): void {
this.sendFireAndForget('call_sdp', {
call_id: callId,
sdp_type: sdpType,
sdp,
});
}
sendCallICE(callId: string, candidate: string): void {
this.sendFireAndForget('call_ice', {
call_id: callId,
candidate,
});
}
sendCallEnd(callId: string, reason?: string): void {
this.sendFireAndForget('call_end', { call_id: callId, reason });
}
sendCallMute(callId: string, muted: boolean): void {
this.sendFireAndForget('call_mute', { call_id: callId, muted });
}
private handleMessageSent(payload: any): void {
// 找到对应的发送请求并resolve
const pendingMsg = this.pendingMessages.find(p =>
p.type === 'chat' && p.payload.conversation_id === payload.conversation_id
);
if (pendingMsg) {
pendingMsg.resolve(payload);
this.removePendingMessage(pendingMsg.id);
}
}
private handleMessageRecalled(payload: any): void {
const pendingMsg = this.pendingMessages.find(p =>
p.type === 'recall' && p.payload.message_id === payload.message_id
);
if (pendingMsg) {
pendingMsg.resolve(payload);
this.removePendingMessage(pendingMsg.id);
}
}
// 发送消息支持降级到HTTP
async sendMessage(
conversationId: string,
detailType: 'private' | 'group',
segments: MessageSegment[],
replyToId?: string
): Promise<any> {
const payload = {
conversation_id: conversationId,
detail_type: detailType,
segments,
reply_to_id: replyToId,
};
// 如果WebSocket连接正常优先使用WebSocket发送
if (this.isConnected() && !this.isFallbackMode) {
return this.sendViaWebSocket('chat', payload);
}
// 降级到HTTP发送
return this.sendViaHTTP(conversationId, detailType, segments, replyToId);
}
// 标记已读
async markRead(conversationId: string, seq: number): Promise<void> {
const payload = {
conversation_id: conversationId,
seq,
};
if (this.isConnected() && !this.isFallbackMode) {
this.sendViaWebSocket('read', payload).catch(() => {});
}
// 已读不需要等待响应,失败也不影响
}
// 发送输入状态
sendTyping(conversationId: string, isTyping: boolean): void {
const payload = {
conversation_id: conversationId,
is_typing: isTyping,
};
if (this.isConnected() && !this.isFallbackMode) {
this.sendViaWebSocket('typing', payload).catch(() => {});
}
}
// 撤回消息
async recallMessage(messageId: string): Promise<any> {
const payload = { message_id: messageId };
if (this.isConnected() && !this.isFallbackMode) {
return this.sendViaWebSocket('recall', payload);
}
// 降级到HTTP
const { api } = await import('./api');
return api.post('/messages/delete_msg', { message_id: messageId });
}
// 通过WebSocket发送fire-and-forget不等待确认
private sendFireAndForget(type: string, payload: any): void {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
return;
}
const message = { type, payload };
this.ws.send(JSON.stringify(message));
}
// 通过WebSocket发送
private sendViaWebSocket(type: string, payload: any): Promise<any> {
return new Promise((resolve, reject) => {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
reject(new Error('WebSocket not connected'));
return;
}
const messageId = `msg_${++this.messageIdCounter}_${Date.now()}`;
const message = {
type,
payload,
};
// 添加到待处理队列
const pendingMsg: PendingMessage = {
id: messageId,
type,
payload,
resolve,
reject,
timestamp: Date.now(),
};
this.pendingMessages.push(pendingMsg);
// 发送消息
this.ws.send(JSON.stringify(message));
// 设置超时
setTimeout(() => {
const index = this.pendingMessages.findIndex(p => p.id === messageId);
if (index >= 0) {
this.pendingMessages[index].reject(new Error('Message timeout'));
this.removePendingMessage(messageId);
}
}, 10000);
});
}
// 通过HTTP发送降级方案
private async sendViaHTTP(
conversationId: string,
detailType: 'private' | 'group',
segments: MessageSegment[],
replyToId?: string
): Promise<any> {
const { api } = await import('./api');
const body: any = {
detail_type: detailType,
segments,
};
if (replyToId) {
body.reply_to_id = replyToId;
}
return api.post(`/conversations/${conversationId}/messages`, body);
}
// 刷新待发送消息队列
private flushPendingMessages(): void {
if (this.pendingMessages.length === 0) return;
const now = Date.now();
const expiredMessages: PendingMessage[] = [];
const validMessages: PendingMessage[] = [];
// 分离过期和有效的消息
for (const msg of this.pendingMessages) {
if (now - msg.timestamp > 30000) {
expiredMessages.push(msg);
} else {
validMessages.push(msg);
}
}
// 拒绝过期消息
expiredMessages.forEach(msg => {
msg.reject(new Error('Message expired'));
});
// 重新发送有效消息
this.pendingMessages = [];
validMessages.forEach(msg => {
this.sendViaWebSocket(msg.type, msg.payload)
.then(msg.resolve)
.catch(msg.reject);
});
}
private removePendingMessage(id: string): void {
const index = this.pendingMessages.findIndex(p => p.id === id);
if (index >= 0) {
this.pendingMessages.splice(index, 1);
}
}
disconnect(): void {
this.shouldRun = false;
this.connected = false;
this.isConnecting = false;
this.isFallbackMode = false;
this.reconnectAttempts = 0; // 重置重连计数
this.lastEventId = ''; // 重置事件ID
this.lastActivityAt = 0; // 重置活动时间
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.stopHeartbeat();
this.stopReconnect();
this.stopFallbackCheck();
// 拒绝所有待处理消息
this.pendingMessages.forEach(msg => {
msg.reject(new Error('WebSocket disconnected'));
});
this.pendingMessages = [];
}
private scheduleReconnect(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
// 超过最大重试次数,进入降级模式
this.enterFallbackMode();
return;
}
this.stopReconnect();
console.log(`[WebSocket] Reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})`);
this.reconnectTimer = setTimeout(() => {
this.reconnectAttempts += 1;
this.connect();
}, this.reconnectDelay);
}
private stopReconnect() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
isConnected(): boolean {
return this.connected && this.ws?.readyState === WebSocket.OPEN;
}
isInFallbackMode(): boolean {
return this.isFallbackMode;
}
// 进入降级模式
private enterFallbackMode(): void {
if (this.isFallbackMode) return;
this.isFallbackMode = true;
console.log('WebSocket entering fallback mode, using HTTP API');
// 启动降级模式检查定时器定期尝试重连WebSocket
this.startFallbackCheck();
}
// 启动降级模式检查
private startFallbackCheck(): void {
this.stopFallbackCheck();
this.fallbackCheckTimer = setInterval(() => {
if (!this.isConnected()) {
this.reconnectAttempts = 0;
this.connect();
}
}, 30000); // 每30秒尝试重连
}
private stopFallbackCheck(): void {
if (this.fallbackCheckTimer) {
clearInterval(this.fallbackCheckTimer);
this.fallbackCheckTimer = null;
}
}
private emit<T extends WSMessageType>(type: T, message: Extract<WSMessage, { type: T }>) {
const handlers = this.messageHandlers.get(type) || [];
handlers.forEach(h => h(message as WSMessage));
}
on<T extends WSMessageType>(type: T, handler: MessageHandler<Extract<WSMessage, { type: T }>>): () => void {
const list = this.messageHandlers.get(type) || [];
list.push(handler as MessageHandler);
this.messageHandlers.set(type, list);
return () => {
const current = this.messageHandlers.get(type) || [];
const idx = current.indexOf(handler as MessageHandler);
if (idx >= 0) current.splice(idx, 1);
};
}
onConnect(handler: ConnectionHandler): () => void {
this.connectionHandlers.push(handler);
return () => {
const i = this.connectionHandlers.indexOf(handler);
if (i >= 0) this.connectionHandlers.splice(i, 1);
};
}
onDisconnect(handler: ConnectionHandler): () => void {
this.disconnectionHandlers.push(handler);
return () => {
const i = this.disconnectionHandlers.indexOf(handler);
if (i >= 0) this.disconnectionHandlers.splice(i, 1);
};
}
private setupAppStateListener(): void {
if (this.appStateSubscription) return;
this.lastAppState = AppState.currentState;
this.appStateSubscription = AppState.addEventListener('change', (nextState: AppStateStatus) => {
if (this.lastAppState.match(/inactive|background/) && nextState === 'active' && !this.isConnected()) {
this.reconnectAttempts = 0;
this.connect();
}
this.lastAppState = nextState;
});
}
async start(): Promise<boolean> {
this.setupAppStateListener();
return this.connect();
}
stop(): void {
if (this.appStateSubscription) {
this.appStateSubscription.remove();
this.appStateSubscription = null;
}
this.disconnect();
}
private markActivity(): void {
this.lastActivityAt = Date.now();
}
private startHeartbeat(): void {
this.stopHeartbeat();
this.heartbeatTimer = setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }));
}
}, this.heartbeatInterval);
}
private stopHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
private handleDisconnected(): void {
// 防止重复调用
if (!this.connected && !this.ws && !this.isConnecting) {
console.log('[WebSocket] Already disconnected, ignoring');
return;
}
console.log('[WebSocket] Handling disconnect...');
const wasActive = this.connected || this.ws != null || this.isConnecting;
this.isConnecting = false;
this.connected = false;
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.stopHeartbeat();
if (wasActive) {
this.disconnectionHandlers.forEach(h => h());
}
if (this.shouldRun) {
this.scheduleReconnect();
} else {
console.log('[WebSocket] shouldRun is false, not reconnecting');
}
}
}
export const wsService = new WebSocketService();