Files
frontend/src/core/usecases/ProcessMessageUseCase.ts
lan a91637466c
Some checks failed
Frontend CI / build-android-apk (push) Has been cancelled
Frontend CI / build-and-push-web (push) Has been cancelled
Frontend CI / ota-android (push) Has been cancelled
Frontend CI / ota-android (pull_request) Has been skipped
Frontend CI / build-and-push-web (pull_request) Has been cancelled
Frontend CI / build-android-apk (pull_request) Has been cancelled
refactor(messaging): replace WebSocket with SSE for real-time message handling
Replace WebSocket-based real-time communication with Server-Sent Events (SSE) across the messaging infrastructure. This includes:

- Create new SSEClient datasource to manage SSE connections
- Create new SSEMessageHandler to process SSE events
- Update ProcessMessageUseCase to use SSEClient instead of WebSocketClient
- Update MessageManager and MessageStateManager to work with SSE handlers
- Rename connection state variables from `isWebSocketConnected` to `isSSEConnected`
- Update type definitions in dto.ts (WSEventType → SSEEventType, etc.)
- Delete obsolete WebSocketClient and WebSocketMessageHandler files
- Update comments and documentation to reflect SSE terminology

This refactoring aligns with the backend's SSE implementation for better compatibility with React Native's networking capabilities.
2026-03-19 00:56:41 +08:00

657 lines
16 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.
/**
* ProcessMessageUseCase - 处理消息用例
* 处理消息的业务逻辑,协调 Repository 和 SSEClient
* 纯业务逻辑无UI依赖
*/
import { messageRepository } from '../../data/repositories/MessageRepository';
import { sseClient, SSEEventType } from '../../data/datasources/SSEClient';
import {
Message,
Conversation,
GroupNotice,
createMessage,
isMessageFromCurrentUser,
shouldIncrementUnread,
buildGroupNoticeText,
} from '../entities/Message';
import { messageService } from '../../services/messageService';
import { api } from '../../services/api';
// 事件类型定义
export type MessageUseCaseEventType =
| 'message_received'
| 'message_recalled'
| 'typing_status'
| 'group_notice'
| 'connection_changed'
| 'error';
export interface MessageUseCaseEvent {
type: MessageUseCaseEventType;
payload: any;
timestamp: number;
}
export type MessageUseCaseSubscriber = (event: MessageUseCaseEvent) => void;
// 已读状态保护记录
interface ReadStateRecord {
timestamp: number;
version: number;
lastReadSeq: number;
clearTimer?: NodeJS.Timeout;
}
class ProcessMessageUseCase {
private subscribers: Set<MessageUseCaseSubscriber> = new Set();
private unsubscribeFns: Array<() => void> = [];
private processedMessageIds: Set<string> = new Set();
private processedMessageIdsExpiry: Map<string, number> = new Map();
private pendingReadMap: Map<string, ReadStateRecord> = new Map();
private readStateVersion = 0;
private pendingUserRequests: Map<string, Promise<any>> = new Map();
private currentUserId: string | null = null;
// 常量
private readonly READ_STATE_PROTECTION_DELAY = 5000; // 5秒
private readonly MESSAGE_ID_EXPIRY = 5 * 60 * 1000; // 5分钟
/**
* 初始化用例
*/
initialize(currentUserId: string | null): void {
this.currentUserId = currentUserId;
this.setupSSEListeners();
}
/**
* 销毁用例
*/
destroy(): void {
this.unsubscribeFns.forEach((fn) => fn());
this.unsubscribeFns = [];
this.subscribers.clear();
this.processedMessageIds.clear();
this.processedMessageIdsExpiry.clear();
this.pendingReadMap.clear();
this.pendingUserRequests.clear();
}
/**
* 设置SSE监听器
*/
private setupSSEListeners(): void {
// 监听私聊消息
const unsubChat = sseClient.on('chat', (message) => {
this.handleNewMessage(message);
});
// 监听群聊消息
const unsubGroupMessage = sseClient.on('group_message', (message) => {
this.handleNewMessage(message);
});
// 监听私聊已读回执
const unsubRead = sseClient.on('read', (message) => {
this.handleReadReceipt(message);
});
// 监听群聊已读回执
const unsubGroupRead = sseClient.on('group_read', (message) => {
this.handleGroupReadReceipt(message);
});
// 监听私聊消息撤回
const unsubRecall = sseClient.on('recall', (message) => {
this.handleRecallMessage(message);
});
// 监听群聊消息撤回
const unsubGroupRecall = sseClient.on('group_recall', (message) => {
this.handleGroupRecallMessage(message);
});
// 监听群聊输入状态
const unsubGroupTyping = sseClient.on('group_typing', (message) => {
this.handleGroupTyping(message);
});
// 监听群通知
const unsubGroupNotice = sseClient.on('group_notice', (message) => {
this.handleGroupNotice(message);
});
// 监听连接状态
const unsubConnected = sseClient.on('connected', () => {
this.notifySubscribers({
type: 'connection_changed',
payload: { connected: true },
timestamp: Date.now(),
});
});
const unsubDisconnected = sseClient.on('disconnected', () => {
this.notifySubscribers({
type: 'connection_changed',
payload: { connected: false },
timestamp: Date.now(),
});
});
this.unsubscribeFns = [
unsubChat,
unsubGroupMessage,
unsubRead,
unsubGroupRead,
unsubRecall,
unsubGroupRecall,
unsubGroupTyping,
unsubGroupNotice,
unsubConnected,
unsubDisconnected,
];
}
/**
* 处理新消息
*/
private async handleNewMessage(message: any): Promise<void> {
const { conversation_id, id, sender_id, seq, segments, created_at, _isAck } = message;
// 消息去重检查
if (this.isMessageProcessed(id)) {
return;
}
this.markMessageAsProcessed(id);
// 构造消息对象
const newMessage = createMessage({
id,
conversationId: String(conversation_id),
senderId: sender_id,
seq,
segments: segments || [],
createdAt: created_at,
});
// 异步获取发送者信息(如果是群聊且不是当前用户发送的)
if (message.type === 'group_message' && sender_id && sender_id !== this.currentUserId) {
this.enrichMessageWithSender(newMessage);
}
// 异步保存到本地数据库
const isRead = _isAck || sender_id === this.currentUserId;
messageRepository.saveMessage(newMessage, isRead).catch((error) => {
console.error('[ProcessMessageUseCase] 保存消息失败:', error);
});
// 通知订阅者
this.notifySubscribers({
type: 'message_received',
payload: {
message: newMessage,
isCurrentUser: sender_id === this.currentUserId,
isAck: _isAck,
},
timestamp: Date.now(),
});
}
/**
* 丰富消息的发送者信息
*/
private async enrichMessageWithSender(message: Message): Promise<void> {
try {
const user = await this.getSenderInfo(message.senderId);
if (user) {
message.sender = user;
// 重新通知,包含发送者信息
this.notifySubscribers({
type: 'message_received',
payload: {
message,
isCurrentUser: false,
isUpdated: true,
},
timestamp: Date.now(),
});
}
} catch (error) {
console.error('[ProcessMessageUseCase] 获取发送者信息失败:', error);
}
}
/**
* 获取发送者信息(带缓存和去重)
*/
private async getSenderInfo(userId: string): Promise<any | null> {
// 先检查本地缓存
const cachedUser = await messageRepository.getUserCache(userId);
if (cachedUser) {
return cachedUser;
}
// 检查是否已有正在进行的请求
const pendingRequest = this.pendingUserRequests.get(userId);
if (pendingRequest) {
return pendingRequest;
}
// 发起新请求
const request = this.fetchUserInfo(userId);
this.pendingUserRequests.set(userId, request);
try {
const user = await request;
return user;
} finally {
this.pendingUserRequests.delete(userId);
}
}
/**
* 从服务器获取用户信息
*/
private async fetchUserInfo(userId: string): Promise<any | null> {
try {
const response = await api.get(`/users/${userId}`);
if (response.code === 0 && response.data) {
await messageRepository.saveUserCache(response.data);
return response.data;
}
return null;
} catch (error) {
console.error(`[ProcessMessageUseCase] 获取用户信息失败: ${userId}`, error);
return null;
}
}
/**
* 检查消息是否已处理
*/
private isMessageProcessed(messageId: string): boolean {
return this.processedMessageIds.has(messageId);
}
/**
* 标记消息已处理
*/
private markMessageAsProcessed(messageId: string): void {
this.processedMessageIds.add(messageId);
this.processedMessageIdsExpiry.set(messageId, Date.now());
// 定期清理
if (this.processedMessageIds.size > 1000) {
this.cleanupProcessedMessageIds();
}
}
/**
* 清理过期的消息ID
*/
private cleanupProcessedMessageIds(): void {
const now = Date.now();
for (const [id, timestamp] of this.processedMessageIdsExpiry.entries()) {
if (now - timestamp > this.MESSAGE_ID_EXPIRY) {
this.processedMessageIds.delete(id);
this.processedMessageIdsExpiry.delete(id);
}
}
}
/**
* 处理私聊已读回执
*/
private handleReadReceipt(message: any): void {
// 可以在这里处理对方已读的状态更新
}
/**
* 处理群聊已读回执
*/
private handleGroupReadReceipt(message: any): void {
// 可以在这里处理群聊已读状态更新
}
/**
* 处理私聊消息撤回
*/
private handleRecallMessage(message: any): Promise<void> {
const { conversation_id, message_id } = message;
// 更新本地数据库状态
messageRepository
.updateMessageStatus(message_id, 'recalled', true)
.catch((error) => {
console.error('[ProcessMessageUseCase] 更新撤回状态失败:', error);
});
// 通知订阅者
this.notifySubscribers({
type: 'message_recalled',
payload: {
conversationId: String(conversation_id),
messageId: message_id,
isGroup: false,
},
timestamp: Date.now(),
});
return Promise.resolve();
}
/**
* 处理群聊消息撤回
*/
private handleGroupRecallMessage(message: any): Promise<void> {
const { conversation_id, message_id } = message;
// 更新本地数据库状态
messageRepository
.updateMessageStatus(message_id, 'recalled', true)
.catch((error) => {
console.error('[ProcessMessageUseCase] 更新撤回状态失败:', error);
});
// 通知订阅者
this.notifySubscribers({
type: 'message_recalled',
payload: {
conversationId: String(conversation_id),
messageId: message_id,
isGroup: true,
},
timestamp: Date.now(),
});
return Promise.resolve();
}
/**
* 处理群聊输入状态
*/
private handleGroupTyping(message: any): void {
const { group_id, user_id, is_typing } = message;
this.notifySubscribers({
type: 'typing_status',
payload: {
groupId: String(group_id),
userId: user_id,
isTyping: is_typing,
},
timestamp: Date.now(),
});
}
/**
* 处理群通知
*/
private handleGroupNotice(message: any): void {
const { notice_type, group_id, data, timestamp, message_id, seq } = message;
const groupIdStr = String(group_id);
const notice: GroupNotice = {
type: notice_type,
groupId: groupIdStr,
data: data || {},
timestamp: timestamp || Date.now(),
messageId: message_id,
seq,
};
// 通知订阅者
this.notifySubscribers({
type: 'group_notice',
payload: {
notice,
text: buildGroupNoticeText(notice),
},
timestamp: Date.now(),
});
}
/**
* 标记会话已读
*/
async markAsRead(conversationId: string, seq: number): Promise<void> {
const existingRecord = this.pendingReadMap.get(conversationId);
// 使用seq去重
if (existingRecord && seq <= existingRecord.lastReadSeq) {
return;
}
// 清除可能存在的旧定时器
if (existingRecord?.clearTimer) {
clearTimeout(existingRecord.clearTimer);
}
// 递增全局版本号
this.readStateVersion++;
const currentVersion = this.readStateVersion;
// 记录已读状态
this.pendingReadMap.set(conversationId, {
timestamp: Date.now(),
version: currentVersion,
lastReadSeq: seq,
});
// 更新本地数据库
await messageRepository.markConversationAsRead(conversationId);
// 调用API
try {
await messageService.markAsRead(conversationId, seq);
} catch (error) {
console.error('[ProcessMessageUseCase] 标记已读API失败:', error);
this.pendingReadMap.delete(conversationId);
throw error;
}
// 设置延迟清除保护
const clearTimer = setTimeout(() => {
const record = this.pendingReadMap.get(conversationId);
if (record && record.version === currentVersion) {
this.pendingReadMap.delete(conversationId);
}
}, this.READ_STATE_PROTECTION_DELAY);
this.pendingReadMap.set(conversationId, {
timestamp: Date.now(),
version: currentVersion,
lastReadSeq: seq,
clearTimer,
});
}
/**
* 检查会话是否有进行中的已读请求
*/
hasPendingRead(conversationId: string): boolean {
return this.pendingReadMap.has(conversationId);
}
/**
* 获取进行中的已读记录
*/
getPendingReadRecord(conversationId: string): ReadStateRecord | undefined {
return this.pendingReadMap.get(conversationId);
}
/**
* 发送消息
*/
async sendMessage(
conversationId: string,
segments: any[],
options?: { replyToId?: string }
): Promise<Message | null> {
try {
const response = await messageService.sendMessage(conversationId, {
segments,
reply_to_id: options?.replyToId,
});
if (response) {
const message = createMessage({
id: response.id,
conversationId,
senderId: this.currentUserId || '',
seq: response.seq,
segments,
createdAt: new Date().toISOString(),
});
// 保存到本地数据库
await messageRepository.saveMessage(message, true);
return message;
}
return null;
} catch (error) {
console.error('[ProcessMessageUseCase] 发送消息失败:', error);
throw error;
}
}
/**
* 获取本地消息
*/
async getLocalMessages(conversationId: string, limit: number = 20): Promise<Message[]> {
return messageRepository.getMessagesByConversation(conversationId, limit);
}
/**
* 获取历史消息
*/
async getHistoryMessages(
conversationId: string,
beforeSeq: number,
limit: number = 20
): Promise<Message[]> {
// 先从本地获取
const localMessages = await messageRepository.getMessagesBeforeSeq(
conversationId,
beforeSeq,
limit
);
if (localMessages.length >= limit) {
return localMessages;
}
// 本地数据不足,从服务端获取
try {
const response = await messageService.getMessages(
conversationId,
undefined,
beforeSeq,
limit
);
if (response?.messages && response.messages.length > 0) {
const serverMessages = response.messages.map((m: any) =>
createMessage({
id: m.id,
conversationId: m.conversation_id || conversationId,
senderId: m.sender_id,
seq: m.seq,
segments: m.segments || [],
createdAt: m.created_at,
status: m.status || 'normal',
})
);
// 保存到本地
await messageRepository.saveMessages(serverMessages, false);
return serverMessages;
}
} catch (error) {
console.error('[ProcessMessageUseCase] 获取历史消息失败:', error);
}
return localMessages;
}
/**
* 从服务端同步消息
*/
async syncMessagesFromServer(
conversationId: string,
afterSeq?: number
): Promise<Message[]> {
try {
const response = await messageService.getMessages(conversationId, afterSeq);
if (response?.messages && response.messages.length > 0) {
const messages = response.messages.map((m: any) =>
createMessage({
id: m.id,
conversationId: m.conversation_id || conversationId,
senderId: m.sender_id,
seq: m.seq,
segments: m.segments || [],
createdAt: m.created_at,
status: m.status || 'normal',
})
);
// 保存到本地
await messageRepository.saveMessages(messages, false);
return messages;
}
} catch (error) {
console.error('[ProcessMessageUseCase] 同步消息失败:', error);
}
return [];
}
/**
* 订阅事件
*/
subscribe(subscriber: MessageUseCaseSubscriber): () => void {
this.subscribers.add(subscriber);
return () => {
this.subscribers.delete(subscriber);
};
}
/**
* 通知订阅者
*/
private notifySubscribers(event: MessageUseCaseEvent): void {
this.subscribers.forEach((subscriber) => {
try {
subscriber(event);
} catch (error) {
console.error('[ProcessMessageUseCase] 订阅者执行失败:', error);
}
});
}
/**
* 获取当前用户ID
*/
getCurrentUserId(): string | null {
return this.currentUserId;
}
/**
* 设置当前用户ID
*/
setCurrentUserId(userId: string | null): void {
this.currentUserId = userId;
}
}
export const processMessageUseCase = new ProcessMessageUseCase();
export default processMessageUseCase;