feat(im): 优化发送中的能力 v0.1:各种清理时的边界

im
YunaiV 2026-05-06 08:22:41 +08:00
parent 3836467481
commit c15d75ba91
1 changed files with 75 additions and 44 deletions

View File

@ -19,6 +19,9 @@ import { useGroupStore } from './groupStore'
import { useDraftStore } from './draftStore' import { useDraftStore } from './draftStore'
import type { Conversation, ConversationStoreMeta, Message } from '../types' import type { Conversation, ConversationStoreMeta, Message } from '../types'
// TODO @芋艿:单个 conversation 的消息过多后,可能存储起来会很慢,后续看看怎么优化。
// TODO @芋艿首次拉取消息时如果消息过多可能导致渲染卡顿。1% 场景)
/** /**
* lastSenderDisplayName caller conversation.lastSenderDisplayName * lastSenderDisplayName caller conversation.lastSenderDisplayName
* *
@ -61,8 +64,57 @@ function deriveLastSenderDisplayName(
return isSameSender ? conversation.lastSenderDisplayName : undefined return isSameSender ? conversation.lastSenderDisplayName : undefined
} }
// TODO @芋艿:单个 conversation 的消息过多后,可能存储起来会很慢,后续看看怎么优化。 /**
// TODO @芋艿首次拉取消息时如果消息过多可能导致渲染卡顿。1% 场景) * conversation.messages last* /
*
* / loadConversations drop
* lastSendTime conversation removeMessage
*/
function recomputeConversationLast(conversation: Conversation): void {
const last = conversation.messages[conversation.messages.length - 1]
if (last) {
const senderDisplayName = deriveLastSenderDisplayName(conversation, last.senderId)
conversation.lastContent = resolveConversationLastContent(
last,
conversation.type,
conversation.targetId,
senderDisplayName
)
conversation.lastSendTime = last.sendTime || conversation.lastSendTime
conversation.lastSenderId = last.senderId
conversation.lastMessageType = last.type
conversation.lastSelfSend = last.selfSend
conversation.lastSenderDisplayName = senderDisplayName
} else {
conversation.lastContent = ''
conversation.lastSenderId = undefined
conversation.lastMessageType = undefined
conversation.lastSelfSend = undefined
conversation.lastSenderDisplayName = undefined
}
}
/**
* REST ack / WS / pull
*
* - content revoke blob URL url File
* - SENDING uploadProgress isUploading
* - FAILED _localFileFAILED uploadAndSendMedia
*
* ackMessage / insertMessage(existingIndex ) 使 REST / WS
*/
function applyServerMessageUpdate(message: Message, updates: Partial<Message>): void {
if (updates.content && updates.content !== message.content) {
revokeBlobUrlsInContent(message.content)
}
Object.assign(message, updates)
if (updates.status !== undefined && updates.status !== ImMessageStatus.SENDING) {
message.uploadProgress = undefined
if (updates.status !== ImMessageStatus.FAILED) {
message._localFile = undefined
}
}
}
export const useConversationStore = defineStore('imConversationStore', { export const useConversationStore = defineStore('imConversationStore', {
state: () => ({ state: () => ({
@ -157,9 +209,15 @@ export const useConversationStore = defineStore('imConversationStore', {
message.status = ImMessageStatus.FAILED message.status = ImMessageStatus.FAILED
return true return true
} }
return !(message.status === ImMessageStatus.FAILED && isMedia); return !(message.status === ImMessageStatus.FAILED && isMedia)
}) })
return { ...conversation, messages } const restored: Conversation = { ...conversation, messages }
// 媒体占位被 drop 时conversation 旧 lastContent / lastSendTime 等仍指向已不存在的占位,
// 按剩余消息末尾重算,避免会话列表显示一条摘要、消息面板里却没对应消息
if (messages.length !== rawMessages.length) {
recomputeConversationLast(restored)
}
return restored
} catch (e) { } catch (e) {
console.warn( console.warn(
'[IM] 单会话消息加载失败', '[IM] 单会话消息加载失败',
@ -348,6 +406,13 @@ export const useConversationStore = defineStore('imConversationStore', {
if (this.activeConversation === conversation) { if (this.activeConversation === conversation) {
this.activeConversation = null this.activeConversation = null
} }
// 释放媒体占位的 blob URL + _localFile未持久化资源软删后没人渲染留着只占内存视频几百 MB
// 同步清空 messages 让 GC 早回收(软删的会话被 getSortedConversations 过滤messages 留着无意义)
conversation.messages.forEach((message) => {
revokeBlobUrlsInContent(message.content)
message._localFile = undefined
})
conversation.messages = []
conversation.deleted = true conversation.deleted = true
// 软删后会话的消息文件不再有用,物理删除该 key // 软删后会话的消息文件不再有用,物理删除该 key
this.removeConversationMessages(type, targetId) this.removeConversationMessages(type, targetId)
@ -417,11 +482,9 @@ export const useConversationStore = defineStore('imConversationStore', {
) )
}) })
if (existingIndex >= 0) { if (existingIndex >= 0) {
// 覆盖更新:服务端字段优先,本地已有的扩展字段(如 selfSend保留 // 覆盖更新:与 ackMessage 走同一份 applyServerMessageUpdate
conversation.messages[existingIndex] = { // WebSocket / pull 比 REST ack 先到的场景下blob revoke 和 uploadProgress / _localFile 清理在这里完成
...conversation.messages[existingIndex], applyServerMessageUpdate(conversation.messages[existingIndex], messageInfo)
...messageInfo
}
conversation.lastSendTime = messageInfo.sendTime || conversation.lastSendTime conversation.lastSendTime = messageInfo.sendTime || conversation.lastSendTime
this.updateMaxId(conversationInfo.type, messageInfo.id) this.updateMaxId(conversationInfo.type, messageInfo.id)
this.saveConversations(conversation) this.saveConversations(conversation)
@ -515,19 +578,7 @@ export const useConversationStore = defineStore('imConversationStore', {
if (!message) { if (!message) {
return return
} }
// 媒体消息 ack 服务端返回真实 url 时,旧 content 里的 blob URL 不再被渲染,立即释放对应 File 内存 applyServerMessageUpdate(message, updates)
if (updates.content && updates.content !== message.content) {
revokeBlobUrlsInContent(message.content)
}
Object.assign(message, updates)
// ① 状态离开 SENDING 后进度条没意义成功UNREAD/READ和失败FAILED都清掉 uploadProgress
// ② _localFile 区别对待FAILED 留着供重试 uploadAndSendMedia非 FAILED 终态可清
if (updates.status !== undefined && updates.status !== ImMessageStatus.SENDING) {
message.uploadProgress = undefined
if (updates.status !== ImMessageStatus.FAILED) {
message._localFile = undefined
}
}
if (updates.id) { if (updates.id) {
this.updateMaxId(conversationType, updates.id) this.updateMaxId(conversationType, updates.id)
} }
@ -702,29 +753,9 @@ export const useConversationStore = defineStore('imConversationStore', {
// 媒体消息占位 / FAILED 删除时释放 content 里的 blob URL避免 File 对象内存泄漏 // 媒体消息占位 / FAILED 删除时释放 content 里的 blob URL避免 File 对象内存泄漏
revokeBlobUrlsInContent(conversation.messages[index].content) revokeBlobUrlsInContent(conversation.messages[index].content)
conversation.messages.splice(index, 1) conversation.messages.splice(index, 1)
// 如果删的是最后一条,按倒数第二条重算摘要 + 事实索引 // 删的是最后一条时按剩余末尾重算摘要 + 事实索引
if (index === conversation.messages.length) { if (index === conversation.messages.length) {
const last = conversation.messages[conversation.messages.length - 1] recomputeConversationLast(conversation)
if (last) {
const senderDisplayName = deriveLastSenderDisplayName(conversation, last.senderId)
conversation.lastContent = resolveConversationLastContent(
last,
conversation.type,
conversation.targetId,
senderDisplayName
)
conversation.lastSendTime = last.sendTime || conversation.lastSendTime
conversation.lastSenderId = last.senderId
conversation.lastMessageType = last.type
conversation.lastSelfSend = last.selfSend
conversation.lastSenderDisplayName = senderDisplayName
} else {
conversation.lastContent = ''
conversation.lastSenderId = undefined
conversation.lastMessageType = undefined
conversation.lastSelfSend = undefined
conversation.lastSenderDisplayName = undefined
}
} }
this.saveConversations(conversation) this.saveConversations(conversation)
}, },