/** * 消息同步服务 * 处理消息和会话的服务器同步逻辑 */ import type { ConversationResponse, MessageResponse } from '../../types/dto'; import { messageService } from '../../services/messageService'; import { getMessagesByConversation, getMaxSeq, saveMessagesBatch, saveConversationsWithRelatedCache, getMessagesBeforeSeq, } from '../../services/database'; import { type IConversationListPagedSource, SqliteConversationListPagedSource, createRemoteConversationListSource, CONVERSATION_LIST_PAGE_SIZE, } from '../conversationListSources'; import type { IMessageSyncService, IMessageStateManager, MessageManagerConversationListDeps } from './types'; export class MessageSyncService implements IMessageSyncService { private stateManager: IMessageStateManager; private getCurrentUserId: () => string | null; /** 远端会话列表(游标或页码) */ private readonly remoteConversationListSource: IConversationListPagedSource; /** 本地会话列表缓存(SQLite) */ private readonly localConversationListSource: IConversationListPagedSource; /** 正在加载会话列表下一页 */ private loadingMoreConversations = false; /** 聊天页活跃期间延迟的会话列表刷新 */ private deferredConversationRefresh = false; constructor( stateManager: IMessageStateManager, getCurrentUserId: () => string | null, deps?: MessageManagerConversationListDeps ) { this.stateManager = stateManager; this.getCurrentUserId = getCurrentUserId; const pageSize = deps?.remoteListPageSize ?? CONVERSATION_LIST_PAGE_SIZE; this.remoteConversationListSource = deps?.remoteConversationListSource ?? createRemoteConversationListSource( deps?.remoteListKind === 'offset' ? 'offset' : 'cursor', pageSize ); this.localConversationListSource = deps?.localConversationListSource ?? new SqliteConversationListPagedSource(); } /** * 获取会话列表 */ async fetchConversations(forceRefresh = false, source: string = 'unknown'): Promise { if (this.stateManager.isLoading() && !forceRefresh) { return; } if (this.shouldDeferConversationRefresh(source, forceRefresh)) { this.deferredConversationRefresh = true; if (__DEV__) { console.log('[MessageSyncService] defer fetchConversations', { source, activeConversationId: this.stateManager.getActiveConversation(), }); } return; } if (__DEV__) { console.log('[MessageSyncService] fetchConversations', { source, forceRefresh, activeConversationId: this.stateManager.getActiveConversation(), }); } // 非强制刷新且内存为空:先用本地源暖机 const currentState = this.stateManager.getState(); if (!forceRefresh && currentState.conversations.size === 0) { const warmed = await this.hydrateConversationsFromLocalSource(); if (warmed) { this.emitConversationListAndUnreadUpdates(); } } this.stateManager.setLoading(true); const emitLoadingToUi = this.stateManager.getConversations().length === 0; if (emitLoadingToUi) { this.stateManager.notifySubscribers({ type: 'conversations_loading', payload: { loading: true }, timestamp: Date.now(), }); } try { this.remoteConversationListSource.restart(); const page = await this.remoteConversationListSource.loadNext(); currentState.conversations.clear(); page.items.forEach(conv => { const normalizedConv = this.normalizeConversationFromFetch(conv); currentState.conversations.set(normalizedConv.id, normalizedConv); }); this.updateConversationList(); this.recomputeConversationTotalUnread(); this.emitConversationListAndUnreadUpdates(); } catch (error) { console.error('[MessageSyncService] 获取会话列表失败:', error); if (this.stateManager.getConversations().length === 0) { const recovered = await this.hydrateConversationsFromLocalSource(); if (recovered) { this.emitConversationListAndUnreadUpdates(); } } this.stateManager.notifySubscribers({ type: 'error', payload: { error, context: 'fetchConversations' }, timestamp: Date.now(), }); } finally { this.stateManager.setLoading(false); if (emitLoadingToUi) { this.stateManager.notifySubscribers({ type: 'conversations_loading', payload: { loading: false }, timestamp: Date.now(), }); } } } /** * 会话列表加载下一页 */ async loadMoreConversations(): Promise { if (!this.remoteConversationListSource.hasMore) { return; } if (this.loadingMoreConversations || this.stateManager.isLoading()) { return; } this.loadingMoreConversations = true; try { const page = await this.remoteConversationListSource.loadNext(); page.items.forEach(conv => { const normalizedConv = this.normalizeConversationFromFetch(conv); this.stateManager.updateConversation(normalizedConv); }); this.recomputeConversationTotalUnread(); this.emitConversationListAndUnreadUpdates(); } catch (error) { console.error('[MessageSyncService] 加载更多会话失败:', error); this.stateManager.notifySubscribers({ type: 'error', payload: { error, context: 'loadMoreConversations' }, timestamp: Date.now(), }); } finally { this.loadingMoreConversations = false; } } /** * 获取单个会话详情 */ async fetchConversationDetail(conversationId: string): Promise { const normalizedId = this.normalizeConversationId(conversationId); try { const detail = await messageService.getConversationById(normalizedId); if (detail) { const normalizedDetail = this.normalizeConversationFromFetch(detail as ConversationResponse); this.stateManager.updateConversation(normalizedDetail); return normalizedDetail; } return null; } catch (error) { console.error('[MessageSyncService] 获取会话详情失败:', error); return null; } } /** * 获取会话消息(增量同步) */ async fetchMessages(conversationId: string, afterSeq?: number): Promise { // 防止重复加载 if (this.stateManager.isLoadingMessages(conversationId)) { return; } this.stateManager.setLoadingMessages(conversationId, true); try { const existingMessagesAtStart = this.stateManager.getMessages(conversationId); const hasInMemoryMessages = existingMessagesAtStart.length > 0; let baselineMaxSeq = hasInMemoryMessages ? existingMessagesAtStart.reduce((max, m) => Math.max(max, m.seq || 0), 0) : 0; if (!afterSeq) { // 先从本地数据库加载 if (!hasInMemoryMessages) { try { const localMessages = await getMessagesByConversation(conversationId, 20); const localMaxSeq = await getMaxSeq(conversationId); baselineMaxSeq = localMaxSeq; if (localMessages.length > 0) { const formattedMessages: MessageResponse[] = localMessages.map(m => ({ id: m.id, conversation_id: m.conversationId, sender_id: m.senderId, seq: m.seq, segments: m.segments || [], status: m.status as any, created_at: m.createdAt, })); this.stateManager.setMessages(conversationId, formattedMessages); this.stateManager.notifySubscribers({ type: 'messages_updated', payload: { conversationId, messages: formattedMessages, source: 'local', }, timestamp: Date.now(), }); } else { // 冷启动兜底:先下发空列表事件 this.stateManager.setMessages(conversationId, []); this.stateManager.notifySubscribers({ type: 'messages_updated', payload: { conversationId, messages: [], source: 'local_empty', }, timestamp: Date.now(), }); } } catch (error) { console.warn('[MessageSyncService] 读取本地消息失败:', error); } } // 服务端快照 + 增量同步 try { const snapshotResp = await messageService.getMessages(conversationId, undefined, undefined, 50); const snapshotMessages = snapshotResp?.messages || []; if (snapshotMessages.length > 0) { const existingMessages = this.stateManager.getMessages(conversationId); const mergedSnapshot = this.mergeMessagesById(existingMessages, snapshotMessages); this.stateManager.setMessages(conversationId, mergedSnapshot); this.stateManager.notifySubscribers({ type: 'messages_updated', payload: { conversationId, messages: mergedSnapshot, newMessages: snapshotMessages, source: 'server_snapshot', }, timestamp: Date.now(), }); // 持久化到本地 saveMessagesBatch(snapshotMessages.map((m: any) => ({ id: m.id, conversationId: m.conversation_id || conversationId, senderId: m.sender_id, content: m.content, type: m.type || 'text', isRead: m.is_read || false, createdAt: m.created_at, seq: m.seq, status: m.status || 'normal', segments: m.segments, }))).catch(error => { console.error('[MessageSyncService] 保存快照消息到本地失败:', error); }); } // 增量补齐 const snapshotMaxSeq = snapshotMessages.reduce((max, m) => Math.max(max, m.seq || 0), 0); if (snapshotMaxSeq > baselineMaxSeq) { const incrementalResp = await messageService.getMessages(conversationId, baselineMaxSeq); const newMessages = incrementalResp?.messages || []; if (newMessages.length > 0) { const existingMessages = this.stateManager.getMessages(conversationId); const mergedMessages = this.mergeMessagesById(existingMessages, newMessages); this.stateManager.setMessages(conversationId, mergedMessages); this.stateManager.notifySubscribers({ type: 'messages_updated', payload: { conversationId, messages: mergedMessages, newMessages, source: 'server_incremental', }, timestamp: Date.now(), }); saveMessagesBatch(newMessages.map((m: any) => ({ id: m.id, conversationId: m.conversation_id || conversationId, senderId: m.sender_id, content: m.content, type: m.type || 'text', isRead: m.is_read || false, createdAt: m.created_at, seq: m.seq, status: m.status || 'normal', segments: m.segments, }))).catch(error => { console.error('[MessageSyncService] 保存增量消息到本地失败:', error); }); } } } catch (error) { console.error('[MessageSyncService] 快照/增量同步失败:', error); } } else { // 指定了 afterSeq const response = await messageService.getMessages(conversationId, afterSeq); if (response?.messages && response.messages.length > 0) { const newMessages = response.messages; const existingMessages = this.stateManager.getMessages(conversationId); const mergedMessages = this.mergeMessagesById(existingMessages, newMessages); this.stateManager.setMessages(conversationId, mergedMessages); this.stateManager.notifySubscribers({ type: 'messages_updated', payload: { conversationId, messages: mergedMessages, newMessages, source: 'server', }, timestamp: Date.now(), }); saveMessagesBatch(newMessages.map((m: any) => ({ id: m.id, conversationId: m.conversation_id || conversationId, senderId: m.sender_id, content: m.content, type: m.type || 'text', isRead: m.is_read || false, createdAt: m.created_at, seq: m.seq, status: m.status || 'normal', segments: m.segments, }))).catch(error => { console.error('[MessageSyncService] 保存消息到本地失败:', error); }); } } } catch (error) { console.error('[MessageSyncService] 获取消息失败:', error); this.stateManager.notifySubscribers({ type: 'error', payload: { error, context: 'fetchMessages', conversationId }, timestamp: Date.now(), }); } finally { this.stateManager.setLoadingMessages(conversationId, false); } } /** * 加载更多历史消息 */ async loadMoreMessages(conversationId: string, beforeSeq: number, limit = 20): Promise { try { // 先从本地获取 const localMessages = await getMessagesBeforeSeq(conversationId, beforeSeq, limit); if (localMessages.length >= limit) { const formattedMessages: MessageResponse[] = [...localMessages].reverse().map(m => ({ id: m.id, conversation_id: m.conversationId, sender_id: m.senderId, seq: m.seq, segments: m.segments || [], status: m.status as any, created_at: m.createdAt, })); const existingMessages = this.stateManager.getMessages(conversationId); const mergedMessages = this.mergeOlderMessages(existingMessages, formattedMessages); this.stateManager.setMessages(conversationId, mergedMessages); this.stateManager.notifySubscribers({ type: 'messages_updated', payload: { conversationId, messages: mergedMessages, source: 'local_history' }, timestamp: Date.now(), }); return formattedMessages; } // 本地数据不足,从服务端获取 const response = await messageService.getMessages(conversationId, undefined, beforeSeq, limit); if (response?.messages && response.messages.length > 0) { const serverMessages = response.messages; await saveMessagesBatch(serverMessages.map((m: any) => ({ id: m.id, conversationId: m.conversation_id || conversationId, senderId: m.sender_id, content: m.content, type: m.type || 'text', isRead: m.is_read || false, createdAt: m.created_at, seq: m.seq, status: m.status || 'normal', segments: m.segments, }))); const existingMessages = this.stateManager.getMessages(conversationId); const mergedMessages = this.mergeOlderMessages(existingMessages, serverMessages); this.stateManager.setMessages(conversationId, mergedMessages); this.stateManager.notifySubscribers({ type: 'messages_updated', payload: { conversationId, messages: mergedMessages, source: 'server_history' }, timestamp: Date.now(), }); return serverMessages; } return []; } catch (error) { console.error('[MessageSyncService] 加载更多消息失败:', error); return []; } } /** * 获取未读数 */ async fetchUnreadCount(): Promise { try { const [unreadData, systemUnreadData] = await Promise.all([ messageService.getUnreadCount(), messageService.getSystemUnreadCount(), ]); const totalUnread = unreadData?.total_unread_count ?? 0; const systemUnread = systemUnreadData?.unread_count ?? 0; this.stateManager.setUnreadCount(totalUnread, systemUnread); // 服务端汇总未读为 0 时,清掉内存中残留的红点 if (totalUnread === 0) { const currentState = this.stateManager.getState(); let anyCleared = false; for (const [cid, conv] of currentState.conversations) { if ((conv.unread_count || 0) > 0) { currentState.conversations.set(cid, { ...conv, unread_count: 0 }); anyCleared = true; } } if (anyCleared) { this.updateConversationList(); this.persistConversationListCache(); this.stateManager.notifySubscribers({ type: 'conversations_updated', payload: { conversations: this.stateManager.getConversations() }, timestamp: Date.now(), }); } } this.stateManager.notifySubscribers({ type: 'unread_count_updated', payload: { totalUnreadCount: totalUnread, systemUnreadCount: systemUnread, }, timestamp: Date.now(), }); } catch (error) { console.error('[MessageSyncService] 获取未读数失败:', error); } } /** * 检查是否可加载更多会话 */ canLoadMoreConversations(): boolean { return this.remoteConversationListSource.hasMore; } /** * 重启数据源 */ restartSources(): void { this.remoteConversationListSource.restart(); this.localConversationListSource.restart(); this.loadingMoreConversations = false; } // ==================== 私有工具方法 ==================== private shouldDeferConversationRefresh(source: string, forceRefresh: boolean): boolean { if (!this.stateManager.getActiveConversation()) return false; if (!forceRefresh) return false; return source === 'sse-reconnect' || source === 'prefetch' || source === 'hooks-initial-refresh'; } private normalizeConversationId(conversationId: string | number | null | undefined): string { return conversationId == null ? '' : String(conversationId); } private normalizeConversationFromFetch(conv: ConversationResponse): ConversationResponse { const id = this.normalizeConversationId(conv.id); return { ...conv, id, }; } private updateConversationList(): void { const currentState = this.stateManager.getState(); const list = Array.from(currentState.conversations.values()).sort((a, b) => { const aPinned = a.is_pinned ? 1 : 0; const bPinned = b.is_pinned ? 1 : 0; if (aPinned !== bPinned) { return bPinned - aPinned; } const aTime = new Date(a.last_message_at || a.updated_at || 0).getTime(); const bTime = new Date(b.last_message_at || b.updated_at || 0).getTime(); return bTime - aTime; }); currentState.conversationList = list; } private recomputeConversationTotalUnread(): void { const currentState = this.stateManager.getState(); currentState.totalUnreadCount = Array.from(currentState.conversations.values()).reduce( (sum, conv) => sum + (conv.unread_count || 0), 0 ); } private emitConversationListAndUnreadUpdates(): void { this.persistConversationListCache(); this.stateManager.notifySubscribers({ type: 'conversations_updated', payload: { conversations: this.stateManager.getConversations() }, timestamp: Date.now(), }); const unreadCount = this.stateManager.getUnreadCount(); this.stateManager.notifySubscribers({ type: 'unread_count_updated', payload: { totalUnreadCount: unreadCount.total, systemUnreadCount: unreadCount.system, }, timestamp: Date.now(), }); } private persistConversationListCache(): void { const currentState = this.stateManager.getState(); const list = Array.from(currentState.conversations.values()); const users = list.flatMap(conv => [ ...(conv.participants || []), ...(conv.last_message?.sender ? [conv.last_message.sender] : []), ]); const groups = list .map(conv => conv.group) .filter((group): group is NonNullable => Boolean(group)); saveConversationsWithRelatedCache(list, users, groups).catch(error => { console.error('[MessageSyncService] 持久化会话列表失败:', error); }); } private async hydrateConversationsFromLocalSource(): Promise { this.localConversationListSource.restart(); const page = await this.localConversationListSource.loadNext(); if (!page.items.length) { return false; } const currentState = this.stateManager.getState(); currentState.conversations.clear(); page.items.forEach(conv => { const normalizedConv = this.normalizeConversationFromFetch(conv); currentState.conversations.set(normalizedConv.id, normalizedConv); }); this.updateConversationList(); this.recomputeConversationTotalUnread(); return true; } private mergeMessagesById(base: MessageResponse[], incoming: MessageResponse[]): MessageResponse[] { if (incoming.length === 0) return base; const merged = new Map(); [...base, ...incoming].forEach(msg => { merged.set(String(msg.id), msg); }); return Array.from(merged.values()).sort((a, b) => a.seq - b.seq); } private mergeOlderMessages(existing: MessageResponse[], incoming: MessageResponse[]): MessageResponse[] { if (incoming.length === 0) return existing; const incomingAsc = [...incoming].sort((a, b) => a.seq - b.seq); if (existing.length === 0) return incomingAsc; const existingMinSeq = existing[0]?.seq ?? Number.MAX_SAFE_INTEGER; const incomingMaxSeq = incomingAsc[incomingAsc.length - 1]?.seq ?? Number.MIN_SAFE_INTEGER; if (incomingMaxSeq < existingMinSeq) { return [...incomingAsc, ...existing]; } return this.mergeMessagesById(existing, incomingAsc); } }