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 type { Conversation, ConversationStoreMeta, Message } from '../types'
// TODO @芋艿:单个 conversation 的消息过多后,可能存储起来会很慢,后续看看怎么优化。
// TODO @芋艿首次拉取消息时如果消息过多可能导致渲染卡顿。1% 场景)
/**
* lastSenderDisplayName caller conversation.lastSenderDisplayName
*
@ -61,8 +64,57 @@ function deriveLastSenderDisplayName(
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', {
state: () => ({
@ -157,9 +209,15 @@ export const useConversationStore = defineStore('imConversationStore', {
message.status = ImMessageStatus.FAILED
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) {
console.warn(
'[IM] 单会话消息加载失败',
@ -348,6 +406,13 @@ export const useConversationStore = defineStore('imConversationStore', {
if (this.activeConversation === conversation) {
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
// 软删后会话的消息文件不再有用,物理删除该 key
this.removeConversationMessages(type, targetId)
@ -417,11 +482,9 @@ export const useConversationStore = defineStore('imConversationStore', {
)
})
if (existingIndex >= 0) {
// 覆盖更新:服务端字段优先,本地已有的扩展字段(如 selfSend保留
conversation.messages[existingIndex] = {
...conversation.messages[existingIndex],
...messageInfo
}
// 覆盖更新:与 ackMessage 走同一份 applyServerMessageUpdate
// WebSocket / pull 比 REST ack 先到的场景下blob revoke 和 uploadProgress / _localFile 清理在这里完成
applyServerMessageUpdate(conversation.messages[existingIndex], messageInfo)
conversation.lastSendTime = messageInfo.sendTime || conversation.lastSendTime
this.updateMaxId(conversationInfo.type, messageInfo.id)
this.saveConversations(conversation)
@ -515,19 +578,7 @@ export const useConversationStore = defineStore('imConversationStore', {
if (!message) {
return
}
// 媒体消息 ack 服务端返回真实 url 时,旧 content 里的 blob URL 不再被渲染,立即释放对应 File 内存
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
}
}
applyServerMessageUpdate(message, updates)
if (updates.id) {
this.updateMaxId(conversationType, updates.id)
}
@ -702,29 +753,9 @@ export const useConversationStore = defineStore('imConversationStore', {
// 媒体消息占位 / FAILED 删除时释放 content 里的 blob URL避免 File 对象内存泄漏
revokeBlobUrlsInContent(conversation.messages[index].content)
conversation.messages.splice(index, 1)
// 如果删的是最后一条,按倒数第二条重算摘要 + 事实索引
// 删的是最后一条时按剩余末尾重算摘要 + 事实索引
if (index === conversation.messages.length) {
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
}
recomputeConversationLast(conversation)
}
this.saveConversations(conversation)
},