From 14613cc973d0701b8fe416284940c1765e0dc3a6 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Sat, 7 Sep 2024 01:59:17 +0800 Subject: [PATCH] refactor: refactor ack logic to not fetch all acks but fetch only the ones that are needed --- client/shared/cache/cache.ts | 34 +++++++++++- client/shared/model/converse.ts | 54 ++++++++++++++++++++ client/shared/model/message.ts | 38 +++++++++++++- client/shared/redux/hooks/useAckInfo.ts | 41 +++++++++++++++ client/shared/redux/hooks/useUnread.ts | 13 ++++- client/shared/redux/setup.ts | 38 ++------------ server/services/core/chat/ack.service.ts | 37 ++++++++++++++ server/services/core/chat/message.service.ts | 12 +++-- 8 files changed, 224 insertions(+), 43 deletions(-) create mode 100644 client/shared/redux/hooks/useAckInfo.ts diff --git a/client/shared/cache/cache.ts b/client/shared/cache/cache.ts index c250958b618..1295fd37657 100644 --- a/client/shared/cache/cache.ts +++ b/client/shared/cache/cache.ts @@ -1,10 +1,15 @@ -import { ChatConverseInfo, fetchConverseInfo } from '../model/converse'; +import { + ChatConverseInfo, + fetchConverseInfo, + getConverseAckInfo, +} from '../model/converse'; import { findGroupInviteByCode, getGroupBasicInfo, GroupBasicInfo, GroupInvite, } from '../model/group'; +import { getConverseLastMessageInfo } from '../model/message'; import { fetchLocalStaticRegistryPlugins, fetchRegistryPlugins, @@ -18,6 +23,7 @@ import { queryClient } from './index'; export enum CacheKey { user = 'user', converse = 'converse', + converseAck = 'converseAck', baseGroupInfo = 'baseGroupInfo', groupInvite = 'groupInvite', pluginRegistry = 'pluginRegistry', @@ -84,6 +90,32 @@ export async function getCachedGroupInviteInfo( return data; } +/** + * 获取缓存的用户信息 + */ +export async function getCachedAckInfo(converseId: string, refetch = false) { + const data = await queryClient.fetchQuery( + [CacheKey.converseAck, converseId], + () => { + return Promise.all([ + getConverseAckInfo([converseId]).then((d) => d[0]), + getConverseLastMessageInfo([converseId]).then((d) => d[0]), + ]).then(([ack, lastMessage]) => { + return { + converseId, + ack, + lastMessage, + }; + }); + }, + { + staleTime: 2 * 1000, // 缓存2s, 减少一秒内的重复请求(无意义) + } + ); + + return data; +} + /** * 获取缓存的插件列表 */ diff --git a/client/shared/model/converse.ts b/client/shared/model/converse.ts index 107fd889531..7445c04a094 100644 --- a/client/shared/model/converse.ts +++ b/client/shared/model/converse.ts @@ -1,4 +1,11 @@ import { request } from '../api/request'; +import { + createAutoMergedRequest, + createAutoSplitRequest, +} from '../utils/request'; +import _uniq from 'lodash/uniq'; +import _flatten from 'lodash/flatten'; +import _zipObject from 'lodash/zipObject'; export enum ChatConverseType { DM = 'DM', // 单人会话 @@ -88,3 +95,50 @@ export async function fetchUserAck(): Promise { return data; } + +/** + * 获取用户存储在远程的会话信息 + */ +export async function fetchUserAckList( + converseIds: string[] +): Promise<(AckInfo | null)[]> { + const { data } = await request.post('/api/chat/ack/list', { + converseIds, + }); + + if (!Array.isArray(data)) { + return []; + } + + return data; +} + +const _fetchConverseAckInfo = createAutoMergedRequest< + string[], + (AckInfo | null)[] +>( + createAutoSplitRequest( + async (converseIdsList) => { + const uniqList = _uniq(_flatten(converseIdsList)); + const infoList = await fetchUserAckList(uniqList); + + const map = _zipObject(uniqList, infoList); + + // 将请求结果根据传输来源重新分组 + return converseIdsList.map((converseIds) => + converseIds.map((converseId) => map[converseId] ?? null) + ); + }, + 'serial', + 100 + ) +); + +/** + * 获取会话信息 + */ +export async function getConverseAckInfo( + converseIds: string[] +): Promise<(AckInfo | null)[]> { + return _fetchConverseAckInfo(converseIds); +} diff --git a/client/shared/model/message.ts b/client/shared/model/message.ts index 023dacb0fb4..35842a25c24 100644 --- a/client/shared/model/message.ts +++ b/client/shared/model/message.ts @@ -1,5 +1,12 @@ import { request } from '../api/request'; import type { ChatMessageReaction, ChatMessage } from 'tailchat-types'; +import { + createAutoMergedRequest, + createAutoSplitRequest, +} from '../utils/request'; +import _uniq from 'lodash/uniq'; +import _flatten from 'lodash/flatten'; +import _zipObject from 'lodash/zipObject'; export { ChatMessageReaction, ChatMessage }; @@ -103,10 +110,15 @@ export async function searchMessage( return data; } +interface LastMessageInfo { + converseId: string; + lastMessageId: string; +} + /** * 基于会话id获取会话最后一条消息的id */ -export async function fetchConverseLastMessages( +async function fetchConverseLastMessages( converseIds: string[] ): Promise<{ converseId: string; lastMessageId: string }[]> { const { data } = await request.post( @@ -119,6 +131,30 @@ export async function fetchConverseLastMessages( return data; } +export const _fetchConverseLastMessageInfo = createAutoMergedRequest< + string[], + (LastMessageInfo | null)[] +>( + createAutoSplitRequest( + async (converseIdsList) => { + const uniqList = _uniq(_flatten(converseIdsList)); + const infoList = await fetchConverseLastMessages(uniqList); + + const map = _zipObject(uniqList, infoList); + + // 将请求结果根据传输来源重新分组 + return converseIdsList.map((converseIds) => + converseIds.map((converseId) => map[converseId] ?? null) + ); + }, + 'serial', + 100 + ) +); +export function getConverseLastMessageInfo(converseIds: string[]) { + return _fetchConverseLastMessageInfo(converseIds); +} + /** * @param converseId 会话ID * @param messageId 消息ID diff --git a/client/shared/redux/hooks/useAckInfo.ts b/client/shared/redux/hooks/useAckInfo.ts new file mode 100644 index 00000000000..2f141190ded --- /dev/null +++ b/client/shared/redux/hooks/useAckInfo.ts @@ -0,0 +1,41 @@ +import { useAppDispatch, useAppSelector } from './useAppSelector'; +import { chatActions } from '../slices'; +import { useEvent } from '../../hooks/useEvent'; +import { getCachedAckInfo } from '../../cache/cache'; + +export function useAckInfoChecker() { + const ack = useAppSelector((state) => state.chat.ack); + const lastMessageMap = useAppSelector((state) => state.chat.lastMessageMap); + const dispatch = useAppDispatch(); + + const ensureAckInfo = useEvent((converseId: string) => { + if ( + ack[converseId] === undefined || + lastMessageMap[converseId] === undefined + ) { + getCachedAckInfo(converseId).then((info) => { + if (info.ack?.lastMessageId) { + dispatch( + chatActions.setConverseAck({ + converseId, + lastMessageId: info.ack.lastMessageId, + }) + ); + } + + if (info.lastMessage?.lastMessageId) { + dispatch( + chatActions.setLastMessageMap([ + { + converseId, + lastMessageId: info.lastMessage.lastMessageId, + }, + ]) + ); + } + }); + } + }); + + return { ensureAckInfo }; +} diff --git a/client/shared/redux/hooks/useUnread.ts b/client/shared/redux/hooks/useUnread.ts index 7d6d91592ee..80729f63daa 100644 --- a/client/shared/redux/hooks/useUnread.ts +++ b/client/shared/redux/hooks/useUnread.ts @@ -1,4 +1,6 @@ import { useAppSelector } from './useAppSelector'; +import { useAckInfoChecker } from './useAckInfo'; +import { useEffect } from 'react'; /** * 返回某些会话是否有未读信息 @@ -6,13 +8,18 @@ import { useAppSelector } from './useAppSelector'; export function useUnread(converseIds: string[]) { const ack = useAppSelector((state) => state.chat.ack); const lastMessageMap = useAppSelector((state) => state.chat.lastMessageMap); + const { ensureAckInfo } = useAckInfoChecker(); - return converseIds.map((converseId) => { + useEffect(() => { + converseIds.forEach((converseId) => ensureAckInfo(converseId)); + }, [converseIds]); + + const unreadList = converseIds.map((converseId) => { if ( ack[converseId] === undefined && lastMessageMap[converseId] !== undefined ) { - // 没有已读记录且远程有数据 + // 远程没有已读记录且获取到了最后一条消息 return true; } @@ -20,4 +27,6 @@ export function useUnread(converseIds: string[]) { // 则返回true(有未读消息) return lastMessageMap[converseId] > ack[converseId]; }); + + return unreadList; } diff --git a/client/shared/redux/setup.ts b/client/shared/redux/setup.ts index cd899c08d09..44639976ec1 100644 --- a/client/shared/redux/setup.ts +++ b/client/shared/redux/setup.ts @@ -9,19 +9,11 @@ import { import type { FriendRequest } from '../model/friend'; import { getCachedConverseInfo } from '../cache/cache'; import type { GroupInfo } from '../model/group'; -import { - ChatMessage, - ChatMessageReaction, - fetchConverseLastMessages, -} from '../model/message'; +import { ChatMessage, ChatMessageReaction } from '../model/message'; import { socketEventListeners } from '../manager/socket'; import { showToasts } from '../manager/ui'; import { t } from '../i18n'; -import { - ChatConverseInfo, - ChatConverseType, - fetchUserAck, -} from '../model/converse'; +import { ChatConverseInfo, ChatConverseType } from '../model/converse'; import { appendUserDMConverse } from '../model/user'; import { sharedEvent } from '../event'; import type { InboxItem } from '../model/inbox'; @@ -61,7 +53,7 @@ function initial(socket: AppSocket, store: AppStore) { console.log('初始化Redux上下文...'); // 立即请求加入房间 - const conversesP = socket + socket .request<{ dmConverseIds: string[]; groupIds: string[]; @@ -77,30 +69,6 @@ function initial(socket: AppSocket, store: AppStore) { throw new Error('findAndJoinRoom failed'); }); - Promise.all([conversesP, fetchUserAck()]).then( - ([{ dmConverseIds, textPanelIds }, acks]) => { - /** - * TODO: 这里的逻辑还需要优化 - * 可能ack和lastMessageMap可以无关? - */ - - // 设置已读消息 - acks.forEach((ackInfo) => { - store.dispatch( - chatActions.setConverseAck({ - converseId: ackInfo.converseId, - lastMessageId: ackInfo.lastMessageId, - }) - ); - }); - - const converseIds = [...dmConverseIds, ...textPanelIds]; - fetchConverseLastMessages(converseIds).then((list) => { - store.dispatch(chatActions.setLastMessageMap(list)); - }); - } - ); - // 获取好友列表 socket .request<{ id: string; nickname?: string }[]>('friend.getAllFriends') diff --git a/server/services/core/chat/ack.service.ts b/server/services/core/chat/ack.service.ts index 44466a15804..1e6f7ca925b 100644 --- a/server/services/core/chat/ack.service.ts +++ b/server/services/core/chat/ack.service.ts @@ -23,6 +23,14 @@ class AckService extends TcService { lastMessageId: 'string', }, }); + this.registerAction('list', this.listAck, { + params: { + converseIds: { + type: 'array', + items: 'string', + }, + }, + }); this.registerAction('all', this.allAck); } @@ -54,6 +62,35 @@ class AckService extends TcService { // TODO: 如果要实现消息已读可以在此处基于会话id进行通知 } + /** + * 所有的ack信息 + */ + async listAck(ctx: TcContext<{ converseIds: string[] }>) { + const userId = ctx.meta.userId; + const { converseIds } = ctx.params; + + const list = await this.adapter.model.find({ + userId, + converseId: { + $in: [...converseIds], + }, + }); + + return converseIds.map((converseId) => { + const lastMessageId = + list + .find((item) => String(item.converseId) === converseId) + ?.lastMessageId?.toString() ?? null; + + return lastMessageId + ? { + converseId, + lastMessageId, + } + : null; + }); + } + /** * 所有的ack信息 */ diff --git a/server/services/core/chat/message.service.ts b/server/services/core/chat/message.service.ts index 48b70f06741..663b0f055b6 100644 --- a/server/services/core/chat/message.service.ts +++ b/server/services/core/chat/message.service.ts @@ -479,10 +479,14 @@ class MessageService extends TcService { }) ); - return list.filter(Boolean).map((item) => ({ - converseId: String(item.converseId), - lastMessageId: String(item._id), - })); + return list.map((item) => + item + ? { + converseId: String(item.converseId), + lastMessageId: String(item._id), + } + : null + ); } async addReaction(