Migrate frontend realtime messaging to SSE.

Switch service integrations and screen/store consumers from websocket events to SSE, and ignore generated dist-web artifacts.

Made-with: Cursor
This commit is contained in:
2026-03-10 12:58:23 +08:00
parent 63e32b15a3
commit be84c01abd
25 changed files with 974 additions and 1305 deletions

View File

@@ -13,7 +13,8 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { create } from 'zustand';
import { User } from '../types';
import { authService, resolveAuthApiError, websocketService, LoginRequest, RegisterRequest } from '../services';
import { authService, resolveAuthApiError, LoginRequest, RegisterRequest } from '../services';
import { sseService } from '../services/sseService';
import {
initDatabase,
closeDatabase,
@@ -91,12 +92,12 @@ function resolveLoginError(error: any): string {
return '登录失败,请稍后重试';
}
// ── 启动 WebSocket 服务 ──
async function startWebSocket(): Promise<void> {
// ── 启动 SSE 实时服务 ──
async function startRealtime(): Promise<void> {
try {
await websocketService.start();
await sseService.start();
} catch (error) {
console.error('[AuthStore] 启动 WebSocket 服务失败:', error);
console.error('[AuthStore] 启动 SSE 服务失败:', error);
}
}
@@ -153,8 +154,8 @@ export const useAuthStore = create<AuthState>((set) => ({
error: null,
});
// 5. 启动 WebSocket
await startWebSocket();
// 5. 启动 SSE
await startRealtime();
return true;
} catch (error: any) {
@@ -191,7 +192,7 @@ export const useAuthStore = create<AuthState>((set) => ({
error: null,
});
await startWebSocket();
await startRealtime();
return true;
} catch (error: any) {
@@ -210,8 +211,8 @@ export const useAuthStore = create<AuthState>((set) => ({
try {
// 1. 通知服务端Token 清理在 authService 内部完成)
await authService.logout();
// 2. 停止 WebSocket
websocketService.stop();
// 2. 停止 SSE
sseService.stop();
// 3. 清除 DB 中的用户缓存DB 此时一定已初始化)
await clearCurrentUserCache().catch(() => {});
// 4. 关闭数据库连接
@@ -270,8 +271,8 @@ export const useAuthStore = create<AuthState>((set) => ({
isLoading: false,
});
// 6. 启动 WebSocket
await startWebSocket();
// 6. 启动 SSE
await startRealtime();
} else {
// Token 已失效或不存在
await clearUserId();

View File

@@ -16,7 +16,7 @@
import { ConversationResponse, MessageResponse, MessageSegment, UserDTO } from '../types/dto';
import { messageService } from '../services/messageService';
import {
websocketService,
sseService,
WSChatMessage,
WSGroupChatMessage,
WSReadMessage,
@@ -26,7 +26,7 @@ import {
WSGroupTypingMessage,
WSGroupNoticeMessage,
GroupNoticeType,
} from '../services/websocketService';
} from '../services/sseService';
import {
saveMessage,
saveMessagesBatch,
@@ -39,7 +39,7 @@ import {
CachedMessage,
getUserCache,
saveUserCache,
deleteMessage as deleteMessageFromDb,
updateMessageStatus,
deleteConversation as deleteConversationFromDb,
} from '../services/database';
import { api } from '../services/api';
@@ -326,47 +326,47 @@ class MessageManager {
// 监听私聊消息
websocketService.on('chat', (message: WSChatMessage) => {
sseService.on('chat', (message: WSChatMessage) => {
this.handleNewMessage(message);
});
// 监听群聊消息
websocketService.on('group_message', (message: WSGroupChatMessage) => {
sseService.on('group_message', (message: WSGroupChatMessage) => {
this.handleNewMessage(message);
});
// 监听私聊已读回执
websocketService.on('read', (message: WSReadMessage) => {
sseService.on('read', (message: WSReadMessage) => {
this.handleReadReceipt(message);
});
// 监听群聊已读回执
websocketService.on('group_read', (message: WSGroupReadMessage) => {
sseService.on('group_read', (message: WSGroupReadMessage) => {
this.handleGroupReadReceipt(message);
});
// 监听私聊消息撤回
websocketService.on('recall', (message: WSRecallMessage) => {
sseService.on('recall', (message: WSRecallMessage) => {
this.handleRecallMessage(message);
});
// 监听群聊消息撤回
websocketService.on('group_recall', (message: WSGroupRecallMessage) => {
sseService.on('group_recall', (message: WSGroupRecallMessage) => {
this.handleGroupRecallMessage(message);
});
// 监听群聊输入状态
websocketService.on('group_typing', (message: WSGroupTypingMessage) => {
sseService.on('group_typing', (message: WSGroupTypingMessage) => {
this.handleGroupTyping(message);
});
// 监听群通知
websocketService.on('group_notice', (message: WSGroupNoticeMessage) => {
sseService.on('group_notice', (message: WSGroupNoticeMessage) => {
this.handleGroupNotice(message);
});
// 监听连接状态
websocketService.onConnect(() => {
sseService.onConnect(() => {
this.state.isWebSocketConnected = true;
this.notifySubscribers({
type: 'connection_changed',
@@ -389,7 +389,7 @@ class MessageManager {
}
});
websocketService.onDisconnect(() => {
sseService.onDisconnect(() => {
this.state.isWebSocketConnected = false;
this.notifySubscribers({
type: 'connection_changed',
@@ -761,36 +761,93 @@ class MessageManager {
private handleGroupReadReceipt(message: WSGroupReadMessage): void {
}
/**
* 将指定消息标记为已撤回(保留占位,不删除)
*/
private markMessageAsRecalled(conversationId: string, messageId: string): void {
const normalizedConversationId = this.normalizeConversationId(conversationId);
const messages = this.state.messagesMap.get(normalizedConversationId);
if (!messages) {
return;
}
let changed = false;
const updatedMessages: MessageResponse[] = messages.map((m): MessageResponse => {
if (String(m.id) !== String(messageId) || m.status === 'recalled') {
return m;
}
changed = true;
return {
...m,
status: 'recalled' as MessageResponse['status'],
segments: [],
};
});
if (!changed) {
return;
}
this.state.messagesMap.set(normalizedConversationId, updatedMessages);
this.notifySubscribers({
type: 'messages_updated',
payload: { conversationId: normalizedConversationId, messages: updatedMessages },
timestamp: Date.now(),
});
}
/**
* 如果撤回的是会话最后一条消息,同步会话列表中的 last_message 状态
*/
private syncConversationLastMessageOnRecall(conversationId: string, messageId: string): void {
const normalizedConversationId = this.normalizeConversationId(conversationId);
const conversation = this.state.conversations.get(normalizedConversationId);
if (!conversation?.last_message) {
return;
}
if (String(conversation.last_message.id) !== String(messageId)) {
return;
}
if (conversation.last_message.status === 'recalled') {
return;
}
const updatedConversation: ConversationResponse = {
...conversation,
last_message: {
...conversation.last_message,
status: 'recalled',
},
};
this.state.conversations.set(normalizedConversationId, updatedConversation);
this.updateConversationList();
this.notifySubscribers({
type: 'conversations_updated',
payload: { conversations: this.state.conversationList },
timestamp: Date.now(),
});
}
/**
* 处理私聊消息撤回
*/
private handleRecallMessage(message: WSRecallMessage): void {
const { conversation_id, message_id } = message;
const normalizedConversationId = this.normalizeConversationId(conversation_id);
// 从消息列表中移除被撤回的消息
const messages = this.state.messagesMap.get(conversation_id);
if (messages) {
const updatedMessages = messages.filter(m => m.id !== message_id);
if (updatedMessages.length !== messages.length) {
this.state.messagesMap.set(conversation_id, updatedMessages);
this.notifySubscribers({
type: 'messages_updated',
payload: { conversationId: conversation_id, messages: updatedMessages },
timestamp: Date.now(),
});
}
}
this.markMessageAsRecalled(normalizedConversationId, message_id);
this.syncConversationLastMessageOnRecall(normalizedConversationId, message_id);
// 通知订阅者消息被撤回
this.notifySubscribers({
type: 'message_recalled',
payload: { conversationId: conversation_id, messageId: message_id },
payload: { conversationId: normalizedConversationId, messageId: message_id },
timestamp: Date.now(),
});
// 本地数据库删除
deleteMessageFromDb(message_id).catch(error => {
console.error('[MessageManager] 删除本地消息失败:', error);
// 同步本地数据库状态,避免冷启动后撤回占位丢失
updateMessageStatus(message_id, 'recalled', true).catch(error => {
console.error('[MessageManager] 更新本地消息撤回状态失败:', error);
});
}
@@ -799,31 +856,21 @@ class MessageManager {
*/
private handleGroupRecallMessage(message: WSGroupRecallMessage): void {
const { conversation_id, message_id } = message;
const normalizedConversationId = this.normalizeConversationId(conversation_id);
// 从消息列表中移除被撤回的消息
const messages = this.state.messagesMap.get(conversation_id);
if (messages) {
const updatedMessages = messages.filter(m => m.id !== message_id);
if (updatedMessages.length !== messages.length) {
this.state.messagesMap.set(conversation_id, updatedMessages);
this.notifySubscribers({
type: 'messages_updated',
payload: { conversationId: conversation_id, messages: updatedMessages },
timestamp: Date.now(),
});
}
}
this.markMessageAsRecalled(normalizedConversationId, message_id);
this.syncConversationLastMessageOnRecall(normalizedConversationId, message_id);
// 通知订阅者消息被撤回
this.notifySubscribers({
type: 'message_recalled',
payload: { conversationId: conversation_id, messageId: message_id, isGroup: true },
payload: { conversationId: normalizedConversationId, messageId: message_id, isGroup: true },
timestamp: Date.now(),
});
// 本地数据库删除
deleteMessageFromDb(message_id).catch(error => {
console.error('[MessageManager] 删除本地消息失败:', error);
// 同步本地数据库状态,避免冷启动后撤回占位丢失
updateMessageStatus(message_id, 'recalled', true).catch(error => {
console.error('[MessageManager] 更新本地消息撤回状态失败:', error);
});
}