fix(message): improve message synchronization and reliability
Some checks failed
Frontend CI / ota-android (push) Successful in 1m47s
Frontend CI / ota-ios (push) Successful in 1m49s
Frontend CI / build-and-push-web (push) Failing after 13m31s
Frontend CI / build-android-apk (push) Successful in 29m15s

Refactor the messaging subsystem to enhance data consistency, prevent race conditions during WebSocket reconnection, and ensure accurate unread counts.

- **WebSocket Reliability**: Implement `client_msg_id` for precise message ACK matching, preventing incorrect pending message resolution in high-frequency scenarios.
- **Sync Logic**: Update `WSMessageHandler` and `MessageManager` to use a bootstrapping state, ensuring buffered SSE events are flushed only after the initial synchronization is complete.
- **State Consistency**:
    - Introduce atomic unread count increments to prevent lost updates.
    - Implement conditional rollback for optimistic read receipts, ensuring that failed API calls do not overwrite newer, valid read states.
- **Resource Management**: Add reference counting to `useMessages` hook to prevent premature clearing of loading states during rapid conversation switching or React StrictMode double-invocations.
- **Data Integrity**: Update `UserCacheService` to re-read the latest message list before applying sender info enrichment, ensuring new messages arriving during the async process are correctly processed.
This commit is contained in:
2026-06-06 13:10:08 +08:00
parent 1f7e25349f
commit 5c81795d39
8 changed files with 195 additions and 47 deletions

View File

@@ -100,9 +100,14 @@ interface UseMessagesReturn {
*
* 核心修复:
* 1. useFocusEffect屏幕重新获得焦点时强制同步消息解决"需要退出重进才能看到新消息"的问题
* 2. 清理 isLoadingMessages组件卸载时清除残留的 loading 锁,防止下次进入被阻断
* 2. 引用计数清理 isLoadingMessages仅在最后一个订阅者卸载时清除 loading 锁,
* 避免 StrictMode 双调用或快速切换会话时,第一次 fetch 还没完成就被清锁
* 3. 重入时 forceSync相同 conversationId 重复进入时强制拉取最新消息
*/
// 记录每个 conversationId 上的活跃订阅数,仅在归零时清锁
const loadingLockRefCount: Map<string, number> = new Map();
export function useMessages(conversationId: string | null): UseMessagesReturn {
const normalizedConversationId = conversationId ? String(conversationId) : null;
@@ -128,6 +133,10 @@ export function useMessages(conversationId: string | null): UseMessagesReturn {
return;
}
// 引用计数 +1
const prevCount = loadingLockRefCount.get(normalizedConversationId) ?? 0;
loadingLockRefCount.set(normalizedConversationId, prevCount + 1);
lastActivatedAtRef.current = Date.now();
const isReentry = lastActivatedIdRef.current === normalizedConversationId;
@@ -141,11 +150,17 @@ export function useMessages(conversationId: string | null): UseMessagesReturn {
lastActivatedIdRef.current = normalizedConversationId;
return () => {
// 清活动会话 + 清除残留的 loading 锁
if (messageManager.getActiveConversation() === normalizedConversationId) {
messageManager.setActiveConversation(null);
// 引用计数 -1仅在最后一个订阅者卸载时清活动会话 + 清 loading 锁
const nextCount = (loadingLockRefCount.get(normalizedConversationId) ?? 1) - 1;
if (nextCount <= 0) {
loadingLockRefCount.delete(normalizedConversationId);
if (messageManager.getActiveConversation() === normalizedConversationId) {
messageManager.setActiveConversation(null);
}
useMessageStore.getState().setLoadingMessages(normalizedConversationId, false);
} else {
loadingLockRefCount.set(normalizedConversationId, nextCount);
}
useMessageStore.getState().setLoadingMessages(normalizedConversationId, false);
};
}, [normalizedConversationId]);