🐛 fix(im): 修复主壳初始化期间消息漏拉 / 缓冲回放失效
三处时序竞态修复: - loading=true 提前到 connect 前,避免 WS 早于 pullOnce 推进 maxId 漏拉断线积压 - loading=false 提到 flushBuffer 前,让回放走正常 insertMessage 而非被 push 回 buffer - 加 bootstrapped 守卫,避免 isConnected watcher 在 friend/group 加载完前抢跑 附带:主壳文件名 Index.vue → index.vue 对齐其他模块小写惯例;清理 5 个 TODO @AI。im
parent
8a7991261f
commit
3a77001b42
|
|
@ -756,7 +756,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
|||
children: [
|
||||
{
|
||||
path: 'home',
|
||||
component: () => import('@/views/im/home/Index.vue'),
|
||||
component: () => import('@/views/im/home/index.vue'),
|
||||
name: 'ImHome',
|
||||
redirect: '/im/home/conversation',
|
||||
meta: { hidden: true, title: '聊天' },
|
||||
|
|
|
|||
|
|
@ -175,6 +175,12 @@ export const useMessagePuller = () => {
|
|||
/** 同一时刻只允许一次 pull:Index.vue 的手动调用与重连 watch 触发可能并发,共用同一个 promise 即可去重 */
|
||||
let pullPromise: Promise<void> | null = null
|
||||
|
||||
/**
|
||||
* 首次 pull 是否已完成。仅在置 true 后,isConnected watch 才会触发 pull。
|
||||
* 防止 socket onopen 比 friendStore/groupStore 预拉先到达时,watcher 抢跑导致群消息缺 senderNickName
|
||||
*/
|
||||
let bootstrapped = false
|
||||
|
||||
/** 执行一次全量增量拉取(重入安全:进行中再次调用复用同一个 promise) */
|
||||
const pullOnce = (): Promise<void> => {
|
||||
if (!currentUserId) {
|
||||
|
|
@ -192,23 +198,26 @@ export const useMessagePuller = () => {
|
|||
pullByType(ImConversationType.PRIVATE, conversationStore.privateMessageMaxId),
|
||||
pullByType(ImConversationType.GROUP, conversationStore.groupMessageMaxId)
|
||||
])
|
||||
|
||||
// 回放 WebSocket 在 loading 期间收到的缓冲消息
|
||||
const buffered = wsStore.flushBuffer()
|
||||
for (const item of buffered) {
|
||||
if (item.conversationType === ImConversationType.PRIVATE) {
|
||||
wsStore.handlePrivateMessage(item.payload)
|
||||
} else {
|
||||
wsStore.handleGroupMessage(item.payload)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[IM] 拉取离线消息失败:', e)
|
||||
} finally {
|
||||
// 关闭 buffer 模式必须早于 flushBuffer,否则 handler 看到 loading=true 会把消息又 push 回 buffer
|
||||
conversationStore.loading = false
|
||||
conversationStore.sortConversations()
|
||||
}
|
||||
|
||||
// 回放 WebSocket 在 loading 期间收到的缓冲消息(此刻走正常 insertMessage 路径)
|
||||
const buffered = wsStore.flushBuffer()
|
||||
for (const item of buffered) {
|
||||
if (item.conversationType === ImConversationType.PRIVATE) {
|
||||
wsStore.handlePrivateMessage(item.payload)
|
||||
} else {
|
||||
wsStore.handleGroupMessage(item.payload)
|
||||
}
|
||||
}
|
||||
|
||||
// pull + replay 都完成后再排序,避免回放消息打乱顺序
|
||||
conversationStore.sortConversations()
|
||||
|
||||
// 重连 / 冷启动后补齐当前激活私聊会话的「对方已读位置」
|
||||
// 离线期间错过的 RECEIPT 推送会被这里补回;其他私聊会话等用户点开时由 Index.vue 的 watch 触发
|
||||
const active = conversationStore.activeConversation
|
||||
|
|
@ -229,6 +238,7 @@ export const useMessagePuller = () => {
|
|||
} finally {
|
||||
// 整个 IIFE 全部完成(含已读位置补齐)后才允许下一次 pullOnce 重入
|
||||
pullPromise = null
|
||||
bootstrapped = true
|
||||
}
|
||||
})()
|
||||
return pullPromise
|
||||
|
|
@ -236,12 +246,12 @@ export const useMessagePuller = () => {
|
|||
|
||||
/**
|
||||
* 断网期间 WS 收不到推送,期间产生的消息只能靠拉取接口按 minId 游标补齐;
|
||||
* 首次连接由 Index.vue 显式调 pullOnce,这里订阅 isConnected 的 false→true 转换,覆盖后续每次重连
|
||||
* 首次连接由 Index.vue 显式调 pullOnce 完成 bootstrap,这里仅覆盖之后的重连
|
||||
*/
|
||||
watch(
|
||||
() => wsStore.isConnected,
|
||||
(isConnected) => {
|
||||
if (isConnected) {
|
||||
if (isConnected && bootstrapped) {
|
||||
void pullOnce()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
<!-- TODO @AI:文件名,是不是应该小写? -->
|
||||
<template>
|
||||
<!--
|
||||
IM 外层容器:聊天模块的全屏沉浸式壳
|
||||
- 左侧 ToolBar:头像 + 三 Tab(消息/好友/群聊)+ 底部设置
|
||||
- 右侧 <router-view>:按路由渲染 MessagePage / FriendPage / GroupPage
|
||||
- 挂载全局弹层:UserInfoCard / ContextMenu / ImageViewer
|
||||
- 挂载全局弹层:UserInfoCard / ContextMenu
|
||||
-->
|
||||
<div class="flex w-full h-full overflow-hidden">
|
||||
<ToolBar />
|
||||
|
|
@ -20,7 +19,6 @@
|
|||
</router-view>
|
||||
|
||||
<!-- 全局弹层:由 useImUiStore 统一调度 -->
|
||||
<!-- TODO @AI:【是不是没必要】大图预览改为 <el-image :preview-src-list> 在调用方就地承接,不再全局挂 -->
|
||||
<UserInfoCard />
|
||||
<ContextMenu />
|
||||
</div>
|
||||
|
|
@ -49,27 +47,35 @@ const groupStore = useGroupStore()
|
|||
const { pullOnce } = useMessagePuller()
|
||||
const { readActive, syncPrivateReadStatus } = useMessageSender()
|
||||
|
||||
// TODO @AI:注释
|
||||
/** 初始化:本地缓存恢复 → 远端通信/同步 → 默认视图 */
|
||||
onMounted(async () => {
|
||||
// 1. 从 IndexedDB 恢复本地会话数据(按会话分桶,需 await 以保证后续步骤拿到完整列表)
|
||||
// ========== 1. 本地状态准备 ==========
|
||||
// 1.1 整段初始化期间 loading=true:阻断 saveConversations 抖动写盘 + 让 WS 普通消息进缓冲区,
|
||||
// 避免 connect 后到 pullOnce 之间到达的实时消息推进 maxId,导致 pull 跳过断线积压消息
|
||||
conversationStore.loading = true
|
||||
// 1.2 从 IndexedDB 恢复本地会话数据(按会话分桶,需 await 以保证后续步骤拿到完整列表)
|
||||
await conversationStore.loadConversations()
|
||||
// 2. 建立 WebSocket 长连接(跨 Tab 持续保持,不因路由切换断开)
|
||||
|
||||
// ========== 2. 远端通信 + 数据同步 ==========
|
||||
// 2.1 建立 WebSocket 长连接(跨 Tab 持续保持,不因路由切换断开)
|
||||
wsStore.connect()
|
||||
// 3. 预拉好友 / 群列表:必须 await,pullOnce 内部要靠 friendStore / groupStore 补 senderNickName 和会话 name/avatar
|
||||
// 2.2 预拉好友 / 群列表:必须 await,pullOnce 内部要靠 friendStore / groupStore 补 senderNickName 和会话 name/avatar
|
||||
await Promise.all([
|
||||
friendStore.loadFriends().catch((e) => console.warn('[IM] 预拉好友失败', e)),
|
||||
groupStore.loadGroups().catch((e) => console.warn('[IM] 预拉群列表失败', e))
|
||||
])
|
||||
// 4. 增量拉取离线消息(私聊 + 群聊,使用各自 minId 游标)
|
||||
// 2.3 增量拉取离线消息(私聊 + 群聊,使用各自 minId 游标);pullOnce finally 里把 loading 归位
|
||||
await pullOnce()
|
||||
// 5. 默认选中第一个会话(仅在消息 Tab 可见)
|
||||
|
||||
// ========== 3. 默认视图 ==========
|
||||
// 3.1 默认选中第一个会话(仅在消息 Tab 可见)
|
||||
const sorted = conversationStore.getSortedConversations
|
||||
if (sorted.length > 0 && !conversationStore.activeConversation) {
|
||||
conversationStore.setActiveConversation(sorted[0])
|
||||
}
|
||||
})
|
||||
|
||||
// TODO @AI:注释
|
||||
/** 离开 IM 主壳:主动断 WS(disconnect 内部已清掉 onclose 防自动重连) */
|
||||
onUnmounted(() => {
|
||||
wsStore.disconnect()
|
||||
})
|
||||
|
|
@ -86,9 +92,9 @@ watch(
|
|||
if (!targetId) {
|
||||
return
|
||||
}
|
||||
// TODO @AI:这里增加注释
|
||||
// 本地清零未读 + 上报后端已读,让其它端 / 对方 UI 同步
|
||||
await readActive()
|
||||
// TODO @AI:这里增加注释
|
||||
// 私聊补一次"对方已读到哪条",弥补离线 / 多端漏掉的 RECEIPT 推送
|
||||
if (conversationStore.activeConversation?.type === ImConversationType.PRIVATE) {
|
||||
void syncPrivateReadStatus(targetId)
|
||||
}
|
||||
Loading…
Reference in New Issue