mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
perf(chat): speed up session switch and stabilize message cursor
This commit is contained in:
@@ -1435,6 +1435,7 @@ class ChatService {
|
|||||||
endTime: number = 0,
|
endTime: number = 0,
|
||||||
ascending: boolean = false
|
ascending: boolean = false
|
||||||
): Promise<{ success: boolean; messages?: Message[]; hasMore?: boolean; error?: string }> {
|
): Promise<{ success: boolean; messages?: Message[]; hasMore?: boolean; error?: string }> {
|
||||||
|
let releaseMessageCursorMutex: (() => void) | null = null
|
||||||
try {
|
try {
|
||||||
const connectResult = await this.ensureConnected()
|
const connectResult = await this.ensureConnected()
|
||||||
if (!connectResult.success) {
|
if (!connectResult.success) {
|
||||||
@@ -1448,6 +1449,12 @@ class ChatService {
|
|||||||
await new Promise(resolve => setTimeout(resolve, 1))
|
await new Promise(resolve => setTimeout(resolve, 1))
|
||||||
}
|
}
|
||||||
this.messageCursorMutex = true
|
this.messageCursorMutex = true
|
||||||
|
let mutexReleased = false
|
||||||
|
releaseMessageCursorMutex = () => {
|
||||||
|
if (mutexReleased) return
|
||||||
|
this.messageCursorMutex = false
|
||||||
|
mutexReleased = true
|
||||||
|
}
|
||||||
|
|
||||||
let state = this.messageCursors.get(sessionId)
|
let state = this.messageCursors.get(sessionId)
|
||||||
|
|
||||||
@@ -1486,7 +1493,7 @@ class ChatService {
|
|||||||
|
|
||||||
state = { cursor: cursorResult.cursor, fetched: 0, batchSize, startTime, endTime, ascending }
|
state = { cursor: cursorResult.cursor, fetched: 0, batchSize, startTime, endTime, ascending }
|
||||||
this.messageCursors.set(sessionId, state)
|
this.messageCursors.set(sessionId, state)
|
||||||
this.messageCursorMutex = false
|
releaseMessageCursorMutex?.()
|
||||||
|
|
||||||
// 如果需要跳过消息(offset > 0),逐批获取但不返回
|
// 如果需要跳过消息(offset > 0),逐批获取但不返回
|
||||||
// 注意:仅在 offset === 0 时重建游标最安全;
|
// 注意:仅在 offset === 0 时重建游标最安全;
|
||||||
@@ -1519,7 +1526,6 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
skipped += count
|
skipped += count
|
||||||
state.fetched += count
|
|
||||||
|
|
||||||
// If satisfied offset, break
|
// If satisfied offset, break
|
||||||
if (skipped >= offset) break;
|
if (skipped >= offset) break;
|
||||||
@@ -1532,6 +1538,7 @@ class ChatService {
|
|||||||
if (attempts >= maxSkipAttempts) {
|
if (attempts >= maxSkipAttempts) {
|
||||||
console.error(`[ChatService] 跳过消息超过最大尝试次数: attempts=${attempts}`)
|
console.error(`[ChatService] 跳过消息超过最大尝试次数: attempts=${attempts}`)
|
||||||
}
|
}
|
||||||
|
state.fetched = offset
|
||||||
console.log(`[ChatService] 跳过完成: skipped=${skipped}, fetched=${state.fetched}, buffered=${state.bufferedMessages?.length || 0}`)
|
console.log(`[ChatService] 跳过完成: skipped=${skipped}, fetched=${state.fetched}, buffered=${state.bufferedMessages?.length || 0}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1557,7 +1564,6 @@ class ChatService {
|
|||||||
const nextBatch = await wcdbService.fetchMessageBatch(state.cursor)
|
const nextBatch = await wcdbService.fetchMessageBatch(state.cursor)
|
||||||
if (nextBatch.success && nextBatch.rows) {
|
if (nextBatch.success && nextBatch.rows) {
|
||||||
rows = rows.concat(nextBatch.rows)
|
rows = rows.concat(nextBatch.rows)
|
||||||
state.fetched += nextBatch.rows.length
|
|
||||||
actualHasMore = nextBatch.hasMore === true
|
actualHasMore = nextBatch.hasMore === true
|
||||||
} else if (!nextBatch.success) {
|
} else if (!nextBatch.success) {
|
||||||
console.error('[ChatService] 获取消息批次失败:', nextBatch.error)
|
console.error('[ChatService] 获取消息批次失败:', nextBatch.error)
|
||||||
@@ -1624,14 +1630,15 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.fetched += rows.length
|
state.fetched += rows.length
|
||||||
this.messageCursorMutex = false
|
releaseMessageCursorMutex?.()
|
||||||
|
|
||||||
this.messageCacheService.set(sessionId, filtered)
|
this.messageCacheService.set(sessionId, filtered)
|
||||||
return { success: true, messages: filtered, hasMore }
|
return { success: true, messages: filtered, hasMore }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.messageCursorMutex = false
|
|
||||||
console.error('ChatService: 获取消息失败:', e)
|
console.error('ChatService: 获取消息失败:', e)
|
||||||
return { success: false, error: String(e) }
|
return { success: false, error: String(e) }
|
||||||
|
} finally {
|
||||||
|
releaseMessageCursorMutex?.()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1726,7 +1733,7 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async getLatestMessages(sessionId: string, limit: number = this.messageBatchDefault): Promise<{ success: boolean; messages?: Message[]; error?: string }> {
|
async getLatestMessages(sessionId: string, limit: number = this.messageBatchDefault): Promise<{ success: boolean; messages?: Message[]; hasMore?: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
const connectResult = await this.ensureConnected()
|
const connectResult = await this.ensureConnected()
|
||||||
if (!connectResult.success) {
|
if (!connectResult.success) {
|
||||||
@@ -1757,7 +1764,7 @@ class ChatService {
|
|||||||
await Promise.allSettled(fixPromises)
|
await Promise.allSettled(fixPromises)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, messages: normalized }
|
return { success: true, messages: normalized, hasMore: batch.hasMore === true }
|
||||||
} finally {
|
} finally {
|
||||||
await wcdbService.closeMessageCursor(cursorResult.cursor)
|
await wcdbService.closeMessageCursor(cursorResult.cursor)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -290,6 +290,13 @@ interface GroupMembersPanelCacheEntry {
|
|||||||
includeMessageCounts: boolean
|
includeMessageCounts: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LoadMessagesOptions {
|
||||||
|
preferLatestPath?: boolean
|
||||||
|
deferGroupSenderWarmup?: boolean
|
||||||
|
forceInitialLimit?: number
|
||||||
|
switchRequestSeq?: number
|
||||||
|
}
|
||||||
|
|
||||||
// 全局头像加载队列管理器已移至 src/utils/AvatarLoadQueue.ts
|
// 全局头像加载队列管理器已移至 src/utils/AvatarLoadQueue.ts
|
||||||
// 全局头像加载队列管理器已移至 src/utils/AvatarLoadQueue.ts
|
// 全局头像加载队列管理器已移至 src/utils/AvatarLoadQueue.ts
|
||||||
import { avatarLoadQueue } from '../utils/AvatarLoadQueue'
|
import { avatarLoadQueue } from '../utils/AvatarLoadQueue'
|
||||||
@@ -521,6 +528,8 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
const sessionsRef = useRef<ChatSession[]>([])
|
const sessionsRef = useRef<ChatSession[]>([])
|
||||||
const currentSessionRef = useRef<string | null>(null)
|
const currentSessionRef = useRef<string | null>(null)
|
||||||
const pendingSessionLoadRef = useRef<string | null>(null)
|
const pendingSessionLoadRef = useRef<string | null>(null)
|
||||||
|
const sessionSwitchRequestSeqRef = useRef(0)
|
||||||
|
const initialLoadRequestedSessionRef = useRef<string | null>(null)
|
||||||
const prevSessionRef = useRef<string | null>(null)
|
const prevSessionRef = useRef<string | null>(null)
|
||||||
const isLoadingMessagesRef = useRef(false)
|
const isLoadingMessagesRef = useRef(false)
|
||||||
const isLoadingMoreRef = useRef(false)
|
const isLoadingMoreRef = useRef(false)
|
||||||
@@ -1424,6 +1433,8 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
preloadImageKeysRef.current.clear()
|
preloadImageKeysRef.current.clear()
|
||||||
lastPreloadSessionRef.current = null
|
lastPreloadSessionRef.current = null
|
||||||
pendingSessionLoadRef.current = null
|
pendingSessionLoadRef.current = null
|
||||||
|
initialLoadRequestedSessionRef.current = null
|
||||||
|
sessionSwitchRequestSeqRef.current += 1
|
||||||
setIsSessionSwitching(false)
|
setIsSessionSwitching(false)
|
||||||
setSessionDetail(null)
|
setSessionDetail(null)
|
||||||
setIsRefreshingDetailStats(false)
|
setIsRefreshingDetailStats(false)
|
||||||
@@ -1887,32 +1898,61 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
setIsRefreshingMessages(false)
|
setIsRefreshingMessages(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 消息批量大小控制(保持稳定,避免游标反复重建)
|
||||||
|
|
||||||
|
|
||||||
// 动态游标批量大小控制
|
|
||||||
const currentBatchSizeRef = useRef(50)
|
const currentBatchSizeRef = useRef(50)
|
||||||
|
|
||||||
|
const warmupGroupSenderProfiles = useCallback((usernames: string[], defer = false) => {
|
||||||
|
if (!Array.isArray(usernames) || usernames.length === 0) return
|
||||||
|
|
||||||
|
const runWarmup = () => {
|
||||||
|
const batchPromise = loadContactInfoBatch(usernames)
|
||||||
|
usernames.forEach(username => {
|
||||||
|
if (!senderAvatarLoading.has(username)) {
|
||||||
|
senderAvatarLoading.set(username, batchPromise.then(() => senderAvatarCache.get(username) || null))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
batchPromise.finally(() => {
|
||||||
|
usernames.forEach(username => senderAvatarLoading.delete(username))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defer) {
|
||||||
|
if ('requestIdleCallback' in window) {
|
||||||
|
window.requestIdleCallback(() => {
|
||||||
|
runWarmup()
|
||||||
|
}, { timeout: 1200 })
|
||||||
|
} else {
|
||||||
|
globalThis.setTimeout(runWarmup, 120)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runWarmup()
|
||||||
|
}, [loadContactInfoBatch])
|
||||||
|
|
||||||
// 加载消息
|
// 加载消息
|
||||||
const loadMessages = async (sessionId: string, offset = 0, startTime = 0, endTime = 0, ascending = false) => {
|
const loadMessages = async (
|
||||||
|
sessionId: string,
|
||||||
|
offset = 0,
|
||||||
|
startTime = 0,
|
||||||
|
endTime = 0,
|
||||||
|
ascending = false,
|
||||||
|
options: LoadMessagesOptions = {}
|
||||||
|
) => {
|
||||||
const listEl = messageListRef.current
|
const listEl = messageListRef.current
|
||||||
const session = sessionMapRef.current.get(sessionId)
|
const session = sessionMapRef.current.get(sessionId)
|
||||||
const unreadCount = session?.unreadCount ?? 0
|
const unreadCount = session?.unreadCount ?? 0
|
||||||
|
|
||||||
let messageLimit = 50
|
let messageLimit = currentBatchSizeRef.current
|
||||||
|
|
||||||
if (offset === 0) {
|
if (offset === 0) {
|
||||||
// 初始加载:重置批量大小
|
const preferredLimit = Number.isFinite(options.forceInitialLimit)
|
||||||
currentBatchSizeRef.current = 50
|
? Math.max(10, Math.floor(options.forceInitialLimit as number))
|
||||||
// 首屏优化:消息过多时限制数量
|
: (unreadCount > 99 ? 30 : 40)
|
||||||
messageLimit = unreadCount > 99 ? 30 : 50
|
currentBatchSizeRef.current = preferredLimit
|
||||||
|
messageLimit = preferredLimit
|
||||||
} else {
|
} else {
|
||||||
// 滚动加载:动态递增 (50 -> 100 -> 200)
|
// 同一会话内保持固定批量,避免后端游标因 batch 改变而重建
|
||||||
if (currentBatchSizeRef.current < 100) {
|
|
||||||
currentBatchSizeRef.current = 100
|
|
||||||
} else {
|
|
||||||
currentBatchSizeRef.current = 200
|
|
||||||
}
|
|
||||||
messageLimit = currentBatchSizeRef.current
|
messageLimit = currentBatchSizeRef.current
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1929,12 +1969,19 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
const firstMsgEl = listEl?.querySelector('.message-wrapper') as HTMLElement | null
|
const firstMsgEl = listEl?.querySelector('.message-wrapper') as HTMLElement | null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI.chat.getMessages(sessionId, offset, messageLimit, startTime, endTime, ascending) as {
|
const useLatestPath = offset === 0 && startTime === 0 && endTime === 0 && !ascending && options.preferLatestPath
|
||||||
|
const result = (useLatestPath
|
||||||
|
? await window.electronAPI.chat.getLatestMessages(sessionId, messageLimit)
|
||||||
|
: await window.electronAPI.chat.getMessages(sessionId, offset, messageLimit, startTime, endTime, ascending)
|
||||||
|
) as {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
messages?: Message[];
|
messages?: Message[];
|
||||||
hasMore?: boolean;
|
hasMore?: boolean;
|
||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
if (options.switchRequestSeq && options.switchRequestSeq !== sessionSwitchRequestSeqRef.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (currentSessionRef.current !== sessionId) {
|
if (currentSessionRef.current !== sessionId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1947,8 +1994,7 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
setHasMoreMessages(false)
|
setHasMoreMessages(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预取发送者信息:在关闭加载遮罩前处理
|
// 群聊发送者信息补齐改为非阻塞执行,避免影响首屏切换
|
||||||
const unreadCount = session?.unreadCount ?? 0
|
|
||||||
const isGroup = sessionId.includes('@chatroom')
|
const isGroup = sessionId.includes('@chatroom')
|
||||||
if (isGroup && result.messages.length > 0) {
|
if (isGroup && result.messages.length > 0) {
|
||||||
const unknownSenders = [...new Set(result.messages
|
const unknownSenders = [...new Set(result.messages
|
||||||
@@ -1956,18 +2002,7 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
.map(m => m.senderUsername as string)
|
.map(m => m.senderUsername as string)
|
||||||
)]
|
)]
|
||||||
if (unknownSenders.length > 0) {
|
if (unknownSenders.length > 0) {
|
||||||
|
warmupGroupSenderProfiles(unknownSenders, options.deferGroupSenderWarmup === true)
|
||||||
// 在批量请求前,先将这些发送者标记为加载中,防止 MessageBubble 触发重复请求
|
|
||||||
const batchPromise = loadContactInfoBatch(unknownSenders)
|
|
||||||
unknownSenders.forEach(username => {
|
|
||||||
if (!senderAvatarLoading.has(username)) {
|
|
||||||
senderAvatarLoading.set(username, batchPromise.then(() => senderAvatarCache.get(username) || null))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 确保在请求完成后清理 loading 状态
|
|
||||||
batchPromise.finally(() => {
|
|
||||||
unknownSenders.forEach(username => senderAvatarLoading.delete(username))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1993,15 +2028,7 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
.map(m => m.senderUsername as string)
|
.map(m => m.senderUsername as string)
|
||||||
)]
|
)]
|
||||||
if (unknownSenders.length > 0) {
|
if (unknownSenders.length > 0) {
|
||||||
const batchPromise = loadContactInfoBatch(unknownSenders)
|
warmupGroupSenderProfiles(unknownSenders, false)
|
||||||
unknownSenders.forEach(username => {
|
|
||||||
if (!senderAvatarLoading.has(username)) {
|
|
||||||
senderAvatarLoading.set(username, batchPromise.then(() => senderAvatarCache.get(username) || null))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
batchPromise.finally(() => {
|
|
||||||
unknownSenders.forEach(username => senderAvatarLoading.delete(username))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2042,8 +2069,11 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
setLoadingMessages(false)
|
setLoadingMessages(false)
|
||||||
setLoadingMore(false)
|
setLoadingMore(false)
|
||||||
if (offset === 0 && pendingSessionLoadRef.current === sessionId) {
|
if (offset === 0 && pendingSessionLoadRef.current === sessionId) {
|
||||||
pendingSessionLoadRef.current = null
|
if (!options.switchRequestSeq || options.switchRequestSeq === sessionSwitchRequestSeqRef.current) {
|
||||||
setIsSessionSwitching(false)
|
pendingSessionLoadRef.current = null
|
||||||
|
initialLoadRequestedSessionRef.current = null
|
||||||
|
setIsSessionSwitching(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2088,7 +2118,10 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (session.username === currentSessionId) return
|
if (session.username === currentSessionId) return
|
||||||
|
const switchRequestSeq = sessionSwitchRequestSeqRef.current + 1
|
||||||
|
sessionSwitchRequestSeqRef.current = switchRequestSeq
|
||||||
pendingSessionLoadRef.current = session.username
|
pendingSessionLoadRef.current = session.username
|
||||||
|
initialLoadRequestedSessionRef.current = session.username
|
||||||
setIsSessionSwitching(true)
|
setIsSessionSwitching(true)
|
||||||
setCurrentSession(session.username, { preserveMessages: false })
|
setCurrentSession(session.username, { preserveMessages: false })
|
||||||
void hydrateSessionPreview(session.username)
|
void hydrateSessionPreview(session.username)
|
||||||
@@ -2096,7 +2129,12 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
setJumpStartTime(0)
|
setJumpStartTime(0)
|
||||||
setJumpEndTime(0)
|
setJumpEndTime(0)
|
||||||
setNoMessageTable(false)
|
setNoMessageTable(false)
|
||||||
void loadMessages(session.username, 0, 0, 0)
|
void loadMessages(session.username, 0, 0, 0, false, {
|
||||||
|
preferLatestPath: true,
|
||||||
|
deferGroupSenderWarmup: true,
|
||||||
|
forceInitialLimit: 30,
|
||||||
|
switchRequestSeq
|
||||||
|
})
|
||||||
// 切换会话后回到正常聊天窗口:收起详情侧栏,详情需手动再次展开
|
// 切换会话后回到正常聊天窗口:收起详情侧栏,详情需手动再次展开
|
||||||
setShowDetailPanel(false)
|
setShowDetailPanel(false)
|
||||||
setShowGroupMembersPanel(false)
|
setShowGroupMembersPanel(false)
|
||||||
@@ -2376,8 +2414,15 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentSessionId && messages.length === 0 && !isLoadingMessages && !isLoadingMore && !noMessageTable) {
|
if (currentSessionId && messages.length === 0 && !isLoadingMessages && !isLoadingMore && !noMessageTable) {
|
||||||
|
if (pendingSessionLoadRef.current === currentSessionId) return
|
||||||
|
if (initialLoadRequestedSessionRef.current === currentSessionId) return
|
||||||
|
initialLoadRequestedSessionRef.current = currentSessionId
|
||||||
setHasInitialMessages(false)
|
setHasInitialMessages(false)
|
||||||
loadMessages(currentSessionId, 0)
|
void loadMessages(currentSessionId, 0, 0, 0, false, {
|
||||||
|
preferLatestPath: true,
|
||||||
|
deferGroupSenderWarmup: true,
|
||||||
|
forceInitialLimit: 30
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [currentSessionId, messages.length, isLoadingMessages, isLoadingMore, noMessageTable])
|
}, [currentSessionId, messages.length, isLoadingMessages, isLoadingMore, noMessageTable])
|
||||||
|
|
||||||
|
|||||||
1
src/types/electron.d.ts
vendored
1
src/types/electron.d.ts
vendored
@@ -177,6 +177,7 @@ export interface ElectronAPI {
|
|||||||
getLatestMessages: (sessionId: string, limit?: number) => Promise<{
|
getLatestMessages: (sessionId: string, limit?: number) => Promise<{
|
||||||
success: boolean
|
success: boolean
|
||||||
messages?: Message[]
|
messages?: Message[]
|
||||||
|
hasMore?: boolean
|
||||||
error?: string
|
error?: string
|
||||||
}>
|
}>
|
||||||
getNewMessages: (sessionId: string, minTime: number, limit?: number) => Promise<{
|
getNewMessages: (sessionId: string, minTime: number, limit?: number) => Promise<{
|
||||||
|
|||||||
Reference in New Issue
Block a user