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

pull/881/head
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 终态再清 _localFile(FAILED 留着供重试 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)
},