refactor(core): introduce EventBus and refactor store infrastructure
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 2m46s
Frontend CI / ota-android (push) Successful in 10m34s
Frontend CI / build-android-apk (push) Successful in 1h15m8s

- Add EventBus for decoupled event-driven communication between services
- Add EventSubscriber component for centralized event handling
- Add requestDedupe utility for shared request deduplication
- Refactor api/wsService to use eventBus instead of direct router navigation
- Extract MessageMapper.toCachedMessages for consistent message mapping
- Remove deprecated BaseManager and CacheBus classes
- Improve @mention rendering with memberMap support in segment rendering
- Update hooks to use useRef instead of useState for fetch tracking
This commit is contained in:
lafay
2026-04-12 18:14:29 +08:00
parent 4b5ce1ba21
commit 6610d2f173
28 changed files with 378 additions and 512 deletions

View File

@@ -8,6 +8,7 @@ import { groupService } from '../../services/groupService';
import { groupCacheRepository, userCacheRepository } from '@/database';
import { useGroupManagerStore } from './groupStore';
import { createCacheEntry, isCacheExpired, DEFAULT_TTL } from '../utils/cache';
import { createDedupe } from '../utils/requestDedupe';
import {
IGroupListPagedSource,
IGroupMemberListPagedSource,
@@ -18,6 +19,8 @@ import {
GROUP_MEMBER_LIST_PAGE_SIZE,
} from '../groupListSources';
const dedupe = createDedupe(() => useGroupManagerStore);
function buildMembersKey(groupId: string, page: number, pageSize: number): string {
return `members:${groupId}:${page}:${pageSize}`;
}
@@ -74,7 +77,7 @@ class GroupManager {
}
}
return this.dedupe(`groups:list:${page}:${pageSize}`, async () => {
return dedupe(`groups:list:${page}:${pageSize}`, async () => {
const store = useGroupManagerStore.getState();
const response = await groupService.getGroups(page, pageSize);
const groups = response.list || [];
@@ -87,7 +90,7 @@ class GroupManager {
}
private refreshGroupsInBackground(page = 1, pageSize = GROUP_LIST_PAGE_SIZE): void {
this.dedupe(`groups:bg:list:${page}:${pageSize}`, async () => {
dedupe(`groups:bg:list:${page}:${pageSize}`, async () => {
const store = useGroupManagerStore.getState();
const response = await groupService.getGroups(page, pageSize);
const groups = response.list || [];
@@ -130,7 +133,7 @@ class GroupManager {
}
}
return this.dedupe(`groups:detail:${groupId}`, async () => {
return dedupe(`groups:detail:${groupId}`, async () => {
const store = useGroupManagerStore.getState();
const group = await groupService.getGroup(groupId);
store.setGroup(groupId, group);
@@ -140,7 +143,7 @@ class GroupManager {
}
private refreshGroupInBackground(groupId: string): void {
this.dedupe(`groups:detail:bg:${groupId}`, async () => {
dedupe(`groups:detail:bg:${groupId}`, async () => {
const store = useGroupManagerStore.getState();
const group = await groupService.getGroup(groupId);
store.setGroup(groupId, group);
@@ -179,7 +182,7 @@ class GroupManager {
}
}
return this.dedupe(`groups:members:${key}`, async () => {
return dedupe(`groups:members:${key}`, async () => {
const store = useGroupManagerStore.getState();
const response = await groupService.getMembers(groupId, page, pageSize);
const members = response.list || [];
@@ -202,7 +205,7 @@ class GroupManager {
pageSize = GROUP_MEMBER_LIST_PAGE_SIZE
): void {
const key = buildMembersKey(groupId, page, pageSize);
this.dedupe(`groups:members:bg:${key}`, async () => {
dedupe(`groups:members:bg:${key}`, async () => {
const store = useGroupManagerStore.getState();
const response = await groupService.getMembers(groupId, page, pageSize);
const members = response.list || [];
@@ -240,18 +243,6 @@ class GroupManager {
clear(): void {
useGroupManagerStore.getState().reset();
}
private async dedupe<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
const store = useGroupManagerStore.getState();
const pending = store.getPendingRequest<T>(key);
if (pending) return pending;
const request = fetcher().finally(() => {
useGroupManagerStore.getState().deletePendingRequest(key);
});
store.setPendingRequest(key, request);
return request;
}
}
export const groupManager = new GroupManager();

View File

@@ -1,38 +1,29 @@
/**
* 群组相关 React Hooks
*/
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { useGroupManagerStore } from './groupStore';
import { groupManager } from './GroupManager';
import type { GroupResponse, GroupMemberResponse } from '../../types/dto';
import { GROUP_LIST_PAGE_SIZE, GROUP_MEMBER_LIST_PAGE_SIZE } from '../groupListSources';
// ==================== useGroups ====================
export interface UseGroupsResult {
groups: GroupResponse[];
isLoading: boolean;
refresh: () => Promise<void>;
}
/**
* 获取群组列表 hook
*/
export function useGroups(): UseGroupsResult {
const groups = useGroupManagerStore(state => state.groupsListEntry?.data ?? []);
const isLoading = useGroupManagerStore(state => state.isLoadingGroups);
const [hasFetched, setHasFetched] = useState(false);
const hasFetchedRef = useRef(false);
useEffect(() => {
if (!hasFetched) {
setHasFetched(true);
if (!hasFetchedRef.current) {
hasFetchedRef.current = true;
useGroupManagerStore.getState().setLoadingGroups(true);
groupManager.getGroups(1, GROUP_LIST_PAGE_SIZE).finally(() => {
useGroupManagerStore.getState().setLoadingGroups(false);
});
}
}, [hasFetched]);
}, []);
const refresh = useCallback(async () => {
useGroupManagerStore.getState().setLoadingGroups(true);
@@ -46,17 +37,12 @@ export function useGroups(): UseGroupsResult {
return { groups, isLoading, refresh };
}
// ==================== useGroup ====================
export interface UseGroupResult {
group: GroupResponse | null;
isLoading: boolean;
refresh: () => Promise<GroupResponse | null>;
}
/**
* 获取单个群组 hook
*/
export function useGroup(groupId: string | undefined | null): UseGroupResult {
const group = useGroupManagerStore(state =>
groupId ? (state.groupsMap.get(groupId)?.data ?? null) : null
@@ -64,18 +50,18 @@ export function useGroup(groupId: string | undefined | null): UseGroupResult {
const isLoading = useGroupManagerStore(state =>
groupId ? state.loadingGroupIds.has(groupId) : false
);
const [hasFetched, setHasFetched] = useState(false);
const lastFetchedRef = useRef<string | null>(null);
useEffect(() => {
if (!groupId) return;
if (!hasFetched) {
setHasFetched(true);
if (lastFetchedRef.current !== groupId) {
lastFetchedRef.current = groupId;
useGroupManagerStore.getState().setLoadingGroup(groupId, true);
groupManager.getGroup(groupId).finally(() => {
useGroupManagerStore.getState().setLoadingGroup(groupId, false);
});
}
}, [groupId, hasFetched]);
}, [groupId]);
const refresh = useCallback(async () => {
if (!groupId) return null;
@@ -90,17 +76,12 @@ export function useGroup(groupId: string | undefined | null): UseGroupResult {
return { group, isLoading, refresh };
}
// ==================== useGroupMembers ====================
export interface UseGroupMembersResult {
members: GroupMemberResponse[];
isLoading: boolean;
refresh: () => Promise<void>;
}
/**
* 获取群组成员 hook
*/
export function useGroupMembers(
groupId: string | undefined | null,
page = 1,
@@ -113,18 +94,19 @@ export function useGroupMembers(
const isLoading = useGroupManagerStore(state =>
membersKey ? state.loadingMemberKeys.has(membersKey) : false
);
const [hasFetched, setHasFetched] = useState(false);
const lastFetchedRef = useRef<string | null>(null);
useEffect(() => {
if (!groupId) return;
if (!hasFetched) {
setHasFetched(true);
const key = `${groupId}:${page}:${pageSize}`;
if (lastFetchedRef.current !== key) {
lastFetchedRef.current = key;
useGroupManagerStore.getState().setLoadingMembers(groupId, true, page, pageSize);
groupManager.getMembers(groupId, page, pageSize).finally(() => {
useGroupManagerStore.getState().setLoadingMembers(groupId, false, page, pageSize);
});
}
}, [groupId, page, pageSize, hasFetched]);
}, [groupId, page, pageSize]);
const refresh = useCallback(async () => {
if (!groupId) return;
@@ -137,4 +119,4 @@ export function useGroupMembers(
}, [groupId, page, pageSize]);
return { members, isLoading, refresh };
}
}