fix(im): 收敛离线拉取的实时副作用
- 离线 pull 只还原历史好友、群聊事件气泡,不再重放实时通知副作用 - 好友详情请求增加 in-flight 去重,有效好友已存在时跳过重复拉取 - 修复软删好友重新添加时被本地缓存误跳过的问题 - 群创建通知只拉群详情,群成员改为进入会话后懒加载 - 避免群基础信息缺失或退群时兜底拉取整群成员pull/884/MERGE
parent
ddafacf64d
commit
8ba76813ae
|
|
@ -217,10 +217,8 @@ export const useMessagePuller = () => {
|
|||
})
|
||||
continue
|
||||
}
|
||||
// 特殊:离线 pull 期间入库的 FRIEND_* 帧(目前仅 FRIEND_ADD persistent=true)也要走好友数据分发,
|
||||
// 否则断线期间的好友列表更新会丢失;与 WebSocket 路径 dispatchPrivateFrame 保持对称
|
||||
// 特殊:历史好友事件只还原聊天气泡;好友主数据由好友增量补偿同步
|
||||
if (isFriendNotification(message.type)) {
|
||||
wsStore.handleFriendNotification(message)
|
||||
// 仅 FRIEND_ADD / FRIEND_DELETE 才作为会话气泡入消息列表
|
||||
if (!isFriendChatTip(message.type)) {
|
||||
continue
|
||||
|
|
@ -244,7 +242,6 @@ export const useMessagePuller = () => {
|
|||
})
|
||||
continue
|
||||
}
|
||||
// 其它消息正常入会话消息列表
|
||||
pulledMessages.push({
|
||||
kind: 'insert',
|
||||
conversationInfo: convertGroupConversation(message),
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ onMounted(async () => {
|
|||
// 1.2 打开当前用户 IM DB
|
||||
await initDb()
|
||||
// 1.3 多个 store 并发从 IDB 读取本地缓存
|
||||
const [, , hasCachedFriends, hasCachedGroups, hasCachedChannels] = await Promise.all([
|
||||
const [, , hasFriendRows, hasGroupRows, hasChannelRows] = await Promise.all([
|
||||
conversationStore.loadConversationList(),
|
||||
messageStore.loadMessageCursorList(),
|
||||
friendStore.loadFriendData(),
|
||||
|
|
@ -99,18 +99,18 @@ onMounted(async () => {
|
|||
// 2.2 无缓存(首登 / 切账号回切):必须 await + 失败抛出中断本轮 onMounted,
|
||||
// 否则 pullOnce 会用 senderId 数字给会话起名落到 IDB 后续基本无法自愈;无缓存分支并发 Promise.all 省一个 RTT
|
||||
const requiredFetches: Promise<unknown>[] = []
|
||||
if (hasCachedFriends) {
|
||||
if (hasFriendRows) {
|
||||
void friendStore.pullFriends().catch((e) => console.warn('[IM] 后台增量拉好友失败', e))
|
||||
} else {
|
||||
requiredFetches.push(friendStore.pullFriends())
|
||||
}
|
||||
if (hasCachedGroups) {
|
||||
if (hasGroupRows) {
|
||||
void groupStore.fetchGroupList(true).catch((e) => console.warn('[IM] 后台刷新群列表失败', e))
|
||||
} else {
|
||||
requiredFetches.push(groupStore.fetchGroupList(true))
|
||||
}
|
||||
// 2.3 频道无增量 pull 接口,继续走 list
|
||||
if (hasCachedChannels) {
|
||||
if (hasChannelRows) {
|
||||
void channelStore.fetchChannelList().catch((e) => console.warn('[IM] 后台刷频道列表失败', e))
|
||||
} else {
|
||||
requiredFetches.push(channelStore.fetchChannelList())
|
||||
|
|
|
|||
|
|
@ -39,10 +39,17 @@ let pendingFetchFriends: PendingRequest | null = null
|
|||
let pendingFetchRequests: PendingRequest | null = null
|
||||
/** 当前正在进行的「加载更多申请」请求 */
|
||||
let pendingLoadMoreRequests: PendingRequest | null = null
|
||||
/** 当前正在进行的好友详情请求 */
|
||||
const pendingFetchFriendInfos = new Map<string, Promise<void>>()
|
||||
|
||||
/** clear() 时递增;旧账号那次还没返回的请求 resolve 后比对一致才写 store,防跨账号数据泄漏 */
|
||||
let storeEpoch = 0
|
||||
|
||||
/** 构建好友详情请求去重 key */
|
||||
function getPendingFriendInfoKey(userId: number, friendUserId: number): string {
|
||||
return `${userId}:${friendUserId}`
|
||||
}
|
||||
|
||||
/** 好友通知 payload(对齐后端 BaseFriendNotification + 子类裁减后的字段) */
|
||||
export interface FriendNotificationPayload {
|
||||
operatorUserId: number
|
||||
|
|
@ -303,19 +310,35 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
async fetchFriendInfo(friendUserId: number) {
|
||||
const requestEpoch = storeEpoch
|
||||
const requestUserId = getCurrentUserId()
|
||||
try {
|
||||
const data = await apiGetFriend(friendUserId)
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
// clear() 已切账号:旧请求的好友详情不能再 upsert 进新账号的 friends
|
||||
if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
|
||||
return
|
||||
}
|
||||
this.upsertFriend(convertFriend(data))
|
||||
} catch (e) {
|
||||
console.warn('[IM friendStore] fetchFriendInfo 失败', e)
|
||||
if (!requestUserId) {
|
||||
return
|
||||
}
|
||||
const key = getPendingFriendInfoKey(requestUserId, friendUserId)
|
||||
const inflight = pendingFetchFriendInfos.get(key)
|
||||
if (inflight) {
|
||||
return inflight
|
||||
}
|
||||
const promise = (async () => {
|
||||
try {
|
||||
const data = await apiGetFriend(friendUserId)
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
// clear() 已切账号:旧请求的好友详情不能再 upsert 进新账号的 friends
|
||||
if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
|
||||
return
|
||||
}
|
||||
this.upsertFriend(convertFriend(data))
|
||||
} catch (e) {
|
||||
console.warn('[IM friendStore] fetchFriendInfo 失败', e)
|
||||
}
|
||||
})().finally(() => {
|
||||
if (pendingFetchFriendInfos.get(key) === promise) {
|
||||
pendingFetchFriendInfos.delete(key)
|
||||
}
|
||||
})
|
||||
pendingFetchFriendInfos.set(key, promise)
|
||||
return promise
|
||||
},
|
||||
|
||||
// ==================== 申请-审批 ====================
|
||||
|
|
@ -707,6 +730,9 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
* 本端真正的「对端」是帧上的另一个用户,不是 payload.friendUserId(payload 里固定是 toUserId)。
|
||||
*/
|
||||
applyFriendAddNotification(_payload: FriendNotificationPayload, peerUserId: number) {
|
||||
if (this.isActiveFriend(peerUserId)) {
|
||||
return
|
||||
}
|
||||
void this.fetchFriendInfo(peerUserId)
|
||||
},
|
||||
|
||||
|
|
@ -773,6 +799,7 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
pendingFetchFriends = null
|
||||
pendingFetchRequests = null
|
||||
pendingLoadMoreRequests = null
|
||||
pendingFetchFriendInfos.clear()
|
||||
storeEpoch++
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
|
||||
return
|
||||
}
|
||||
const fresh = (list || []).map(convertGroup)
|
||||
const fresh = (list || []).map((group) => convertGroup(group))
|
||||
// 合并而非全量替换:silent / groupRemark / 成员缓存这些字段不在 ImGroupRespVO 里,得从旧 group 保留
|
||||
const groupMap = new Map(this.groups.map((group) => [group.id, group]))
|
||||
this.groups = fresh.map((group) => {
|
||||
|
|
@ -584,14 +584,14 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
/**
|
||||
* 接收 GROUP_* 群广播事件,按 type 分发到对应私有 action
|
||||
*
|
||||
* WebSocket 实时收 + useMessagePuller 离线 pull 都走 messageStore.insertMessage 旁路调用
|
||||
* WebSocket 实时收走 messageStore.insertMessage 旁路调用
|
||||
* store 里没缓存的群静默忽略,等 fetchGroupList 兜底
|
||||
*/
|
||||
applyGroupNotification(groupId: number, type: number, content?: string) {
|
||||
if (!groupId) {
|
||||
return
|
||||
}
|
||||
let payload: GroupNotificationPayload = {}
|
||||
let payload: GroupNotificationPayload
|
||||
try {
|
||||
payload = content ? JSON.parse(content) : {}
|
||||
} catch (error) {
|
||||
|
|
@ -683,7 +683,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
}
|
||||
},
|
||||
|
||||
/** 创建群广播:创建者多端同步 + 初始成员首次拉取;payload.memberUserIds 含自己 → 拉群详情 / 成员;本端发起者已经 upsert 过本群,跳过避免双拉 */
|
||||
/** 创建群广播:群未就位时拉群详情 */
|
||||
async applyGroupCreateNotification(groupId: number, payload: GroupNotificationPayload) {
|
||||
if (!isSelfInPayloadMembers(payload)) {
|
||||
return
|
||||
|
|
@ -693,9 +693,7 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
if (selfIsOperator && this.getGroup(groupId)) {
|
||||
return
|
||||
}
|
||||
// 先 await fetchGroupInfo 把群 upsert 进 state.groups;否则 fetchGroupMemberList 的「不是我加入的群」guard 会兜空
|
||||
await this.fetchGroupInfo(groupId)
|
||||
this.fetchGroupMemberList(groupId, true).catch(() => undefined)
|
||||
},
|
||||
|
||||
/** 群名变更:按 newName 局部更新本地群名 */
|
||||
|
|
@ -890,7 +888,7 @@ function convertGroup(group: ImGroupRespVO): Group {
|
|||
avatar: group.avatar,
|
||||
notice: group.notice,
|
||||
ownerUserId: group.ownerUserId,
|
||||
pinnedMessages: group.pinnedMessages?.map(convertGroupMessageVO),
|
||||
pinnedMessages: group.pinnedMessages?.map((message) => convertGroupMessageVO(message)),
|
||||
mutedAll: group.mutedAll,
|
||||
banned: group.banned,
|
||||
joinApproval: group.joinApproval,
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ function deriveLastSenderDisplayName(
|
|||
if (conversation.type === ImConversationType.GROUP) {
|
||||
const groupStore = useGroupStore()
|
||||
const group = groupStore.getGroup(conversation.targetId)
|
||||
if (isGroupQuit(group)) {
|
||||
if (!group || isGroupQuit(group)) {
|
||||
return conversation.lastSenderId === senderId ? conversation.lastSenderDisplayName : undefined
|
||||
}
|
||||
const fetchPromise =
|
||||
|
|
@ -501,24 +501,12 @@ export const useMessageStore = defineStore('imMessageStore', {
|
|||
const { conversationInfo } = pulledMessage
|
||||
const hasServerClientMessageId = !!pulledMessage.message.clientMessageId
|
||||
const message = ensureClientMessageId(pulledMessage.message)
|
||||
// 1.2 群通知先同步群资料
|
||||
if (
|
||||
conversationInfo.type === ImConversationType.GROUP &&
|
||||
isGroupNotification(message.type)
|
||||
) {
|
||||
useGroupStore().applyGroupNotification(
|
||||
conversationInfo.targetId,
|
||||
message.type,
|
||||
message.content
|
||||
)
|
||||
}
|
||||
|
||||
// 1.3 确保会话和消息缓存存在
|
||||
// 1.2 确保会话和消息缓存存在
|
||||
const conversation = conversationStore.ensureConversation(conversationInfo)
|
||||
const messages = this.getMessageList(conversationInfo.type, conversationInfo.targetId)
|
||||
const existingIndex = messages.findIndex((existing) => isSameMessage(existing, message))
|
||||
if (existingIndex >= 0) {
|
||||
// 1.4 已存在消息合并服务端状态
|
||||
// 1.3 已存在消息合并服务端状态
|
||||
applyServerMessageUpdate(messages[existingIndex], message)
|
||||
if (existingIndex === messages.length - 1) {
|
||||
recomputeConversationLast(conversation, messages)
|
||||
|
|
@ -530,7 +518,7 @@ export const useMessageStore = defineStore('imMessageStore', {
|
|||
continue
|
||||
}
|
||||
|
||||
// 1.5 新消息更新会话摘要和未读状态
|
||||
// 1.4 新消息更新会话摘要和未读状态
|
||||
applyConversationSummary(conversation, message)
|
||||
syncConversationAtFlags(conversation, message)
|
||||
const isActive =
|
||||
|
|
@ -545,7 +533,7 @@ export const useMessageStore = defineStore('imMessageStore', {
|
|||
conversation.unreadCount++
|
||||
}
|
||||
|
||||
// 1.6 新消息按服务端 id 插入内存列表
|
||||
// 1.5 新消息按服务端 id 插入内存列表
|
||||
let insertIndex = messages.length
|
||||
if (message.id) {
|
||||
for (let index = 0; index < messages.length; index++) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue