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