Files
frontend/src/services/post/PostSyncService.ts

361 lines
13 KiB
TypeScript
Raw Normal View History

import { postRepository } from '../../data/repositories/PostRepository';
import type { Post } from '../../core/entities/Post';
import type {
GetPostsParams,
CreatePostData,
UpdatePostData,
PostsResult,
SearchPostsParams,
} from '../../data/repositories/interfaces/IPostRepository';
import { PaginationStateManager } from '../../infrastructure/pagination/PaginationStateManager';
import type { PaginationState } from '../../infrastructure/pagination/types';
import { usePostListStore, type PostsState } from '../../stores/post/postListStore';
import { mergePostListWithFastPath, mergeRefreshWindow } from './postMerge';
export type { PostsState } from '../../stores/post/postListStore';
export interface PostDetailState {
post: Post | null;
isLoading: boolean;
error: string | null;
}
const DEFAULT_PAGE_SIZE = 20;
const MERGE_PERF_WARN_THRESHOLD_MS = 12;
class PostSyncService {
private static instance: PostSyncService;
private paginationManager: PaginationStateManager;
private currentUserId: string | null = null;
private constructor() {
this.paginationManager = new PaginationStateManager();
}
static getInstance(): PostSyncService {
if (!PostSyncService.instance) {
PostSyncService.instance = new PostSyncService();
}
return PostSyncService.instance;
}
initialize(currentUserId: string | null = null): void {
this.currentUserId = currentUserId;
}
destroy(): void {
usePostListStore.getState().clearAllStates();
this.paginationManager.clear();
this.currentUserId = null;
}
getPostsState(key: string = 'default'): PostsState {
return usePostListStore.getState().getPostsState(key);
}
getPostDetailState(postId: string): PostDetailState {
return usePostListStore.getState().getPostDetailState(postId);
}
applyShareCountUpdate(postId: string, sharesCount: number): void {
usePostListStore.getState().applyShareCountUpdate(postId, sharesCount);
}
async fetchPosts(params?: GetPostsParams, key: string = 'default'): Promise<PostsResult> {
const queryParams: GetPostsParams = {
pageSize: params?.pageSize || DEFAULT_PAGE_SIZE,
...params,
cursor: params?.cursor !== undefined ? params.cursor : '',
};
// 判断参数是否变化:参数变化(切换分区/排序/筛选)时直接清空旧数据,
// 避免渲染期间残留上一个分区的帖子造成闪烁
const prevState = usePostListStore.getState().getPostsState(key);
const paramsChanged = JSON.stringify(prevState.lastParams) !== JSON.stringify(queryParams);
// 参数变化时立即写入空数据 + loading 状态,确保 UI 第一时间进入加载态
// 而不是先渲染上一个分区的旧帖子
usePostListStore.getState().updatePostsState(key, {
isLoading: true,
error: null,
...(paramsChanged ? { posts: [], cursor: null, currentPage: 1, hasMore: true } : {}),
});
try {
const result = await postRepository.getPosts(queryParams);
const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
const currentState = usePostListStore.getState().getPostsState(key);
const incomingPosts = Array.isArray(result.posts) ? result.posts : [];
const mergeStart = Date.now();
// 参数变化时直接替换;同参数刷新则合并以保留滚动位置上的帖子
const mergedPosts = paramsChanged
? incomingPosts
: mergeRefreshWindow(currentState.posts, incomingPosts);
const mergeCost = Date.now() - mergeStart;
if (mergeCost >= MERGE_PERF_WARN_THRESHOLD_MS) {
console.log('[PostPerf] fetchPosts merge cost', { key, costMs: mergeCost, prev: currentState.posts.length, next: incomingPosts.length });
}
usePostListStore.getState().updatePostsState(key, {
posts: mergedPosts,
hasMore: result.hasMore,
cursor: isCursorMode ? result.nextCursor : null,
currentPage: isCursorMode ? undefined : 1,
isLoading: false,
lastLoadTime: Date.now(),
lastParams: params,
});
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '获取帖子列表失败';
usePostListStore.getState().updatePostsState(key, { isLoading: false, error: errorMessage });
throw error;
}
}
async refreshPosts(key: string = 'default'): Promise<PostsResult> {
const state = usePostListStore.getState().getPostsState(key);
usePostListStore.getState().updatePostsState(key, { isRefreshing: true, error: null });
try {
const result = await postRepository.getPosts({
pageSize: DEFAULT_PAGE_SIZE,
...state.lastParams,
cursor: state.lastParams?.cursor !== undefined ? state.lastParams.cursor : '',
});
const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
const incomingPosts = Array.isArray(result.posts) ? result.posts : [];
const mergeStart = Date.now();
// refreshPosts 是刷新操作,应直接替换当前列表,避免旧数据残留
const mergedPosts = incomingPosts;
const mergeCost = Date.now() - mergeStart;
if (mergeCost >= MERGE_PERF_WARN_THRESHOLD_MS) {
console.log('[PostPerf] refreshPosts merge cost', { key, costMs: mergeCost, prev: state.posts.length, next: incomingPosts.length });
}
usePostListStore.getState().updatePostsState(key, {
posts: mergedPosts,
hasMore: result.hasMore,
cursor: isCursorMode ? result.nextCursor : null,
currentPage: isCursorMode ? undefined : 1,
isRefreshing: false,
lastLoadTime: Date.now(),
});
this.paginationManager.reset(key);
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '刷新帖子列表失败';
usePostListStore.getState().updatePostsState(key, { isRefreshing: false, error: errorMessage });
throw error;
}
}
async loadMorePosts(key: string = 'default'): Promise<PostsResult> {
const state = usePostListStore.getState().getPostsState(key);
if (!state.hasMore) return { posts: [], hasMore: false };
if (state.isLoading) return { posts: [], hasMore: state.hasMore };
usePostListStore.getState().updatePostsState(key, { isLoading: true, error: null });
try {
const useCursorMode = state.cursor !== null && state.cursor !== undefined;
const base = { ...(state.lastParams ?? {}) } as GetPostsParams;
let queryParams: GetPostsParams;
if (useCursorMode) {
delete base.page;
queryParams = { ...base, pageSize: DEFAULT_PAGE_SIZE, cursor: state.cursor! };
} else {
const nextPage = (state.currentPage || 1) + 1;
delete base.cursor;
queryParams = { ...base, pageSize: DEFAULT_PAGE_SIZE, page: nextPage };
}
const result = await postRepository.getPosts(queryParams);
const incomingPosts = Array.isArray(result.posts) ? result.posts : [];
const mergeStart = Date.now();
const newPosts = mergePostListWithFastPath(state.posts, incomingPosts);
const mergeCost = Date.now() - mergeStart;
if (mergeCost >= MERGE_PERF_WARN_THRESHOLD_MS) {
console.log('[PostPerf] loadMore merge cost', { key, costMs: mergeCost, prev: state.posts.length, incoming: incomingPosts.length, merged: newPosts.length });
}
const responseIsCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
usePostListStore.getState().updatePostsState(key, {
posts: newPosts,
hasMore: result.hasMore,
cursor: responseIsCursorMode ? result.nextCursor : null,
currentPage: useCursorMode ? state.currentPage : ((state.currentPage || 1) + 1),
isLoading: false,
lastLoadTime: Date.now(),
});
return { posts: incomingPosts, hasMore: result.hasMore, nextCursor: result.nextCursor };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '加载更多帖子失败';
console.error('[PostSyncService] Load more error:', errorMessage);
usePostListStore.getState().updatePostsState(key, { isLoading: false, error: errorMessage });
throw error;
}
}
async fetchPostById(id: string): Promise<Post | null> {
usePostListStore.getState().updatePostDetailState(id, { isLoading: true, error: null });
try {
const post = await postRepository.getPostById(id);
usePostListStore.getState().updatePostDetailState(id, { post, isLoading: false });
return post;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '获取帖子详情失败';
usePostListStore.getState().updatePostDetailState(id, { isLoading: false, error: errorMessage });
throw error;
}
}
async createPost(data: CreatePostData): Promise<Post> {
const post = await postRepository.createPost(data);
const store = usePostListStore.getState();
const newMap = new Map(store.postsStateMap);
for (const [key, postsState] of newMap) {
newMap.set(key, { ...postsState, posts: [post, ...postsState.posts] });
}
usePostListStore.setState({ postsStateMap: newMap });
return post;
}
async updatePost(id: string, data: UpdatePostData): Promise<Post> {
const post = await postRepository.updatePost(id, data);
usePostListStore.getState().updatePostDetailState(id, { post });
usePostListStore.getState().updatePostInLists(id, post);
return post;
}
async deletePost(id: string): Promise<void> {
await postRepository.deletePost(id);
usePostListStore.getState().clearPostDetailState(id);
usePostListStore.getState().removePostFromLists(id);
}
async likePost(id: string): Promise<Post> {
const post = await postRepository.likePost(id);
usePostListStore.getState().updatePostDetailState(id, { post });
usePostListStore.getState().updatePostInLists(id, post);
return post;
}
async unlikePost(id: string): Promise<Post> {
const post = await postRepository.unlikePost(id);
usePostListStore.getState().updatePostDetailState(id, { post });
usePostListStore.getState().updatePostInLists(id, post);
return post;
}
async favoritePost(id: string): Promise<Post> {
const post = await postRepository.favoritePost(id);
usePostListStore.getState().updatePostDetailState(id, { post });
usePostListStore.getState().updatePostInLists(id, post);
return post;
}
async unfavoritePost(id: string): Promise<Post> {
const post = await postRepository.unfavoritePost(id);
usePostListStore.getState().updatePostDetailState(id, { post });
usePostListStore.getState().updatePostInLists(id, post);
return post;
}
async searchPosts(params: SearchPostsParams, key: string = 'search'): Promise<PostsResult> {
usePostListStore.getState().updatePostsState(key, { isLoading: true, error: null });
try {
const result = await postRepository.searchPosts(params);
usePostListStore.getState().updatePostsState(key, {
posts: result.posts,
hasMore: result.hasMore,
cursor: result.nextCursor || null,
isLoading: false,
lastLoadTime: Date.now(),
});
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '搜索帖子失败';
usePostListStore.getState().updatePostsState(key, { isLoading: false, error: errorMessage });
throw error;
}
}
async fetchPostsByUser(userId: string, params?: GetPostsParams, key?: string): Promise<PostsResult> {
const listKey = key || `user_${userId}`;
usePostListStore.getState().updatePostsState(listKey, { isLoading: true, error: null });
try {
const result = await postRepository.getPostsByUser(userId, params);
usePostListStore.getState().updatePostsState(listKey, {
posts: result.posts,
hasMore: result.hasMore,
cursor: result.nextCursor || null,
isLoading: false,
lastLoadTime: Date.now(),
});
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '获取用户帖子失败';
usePostListStore.getState().updatePostsState(listKey, { isLoading: false, error: errorMessage });
throw error;
}
}
clearPostsState(key: string = 'default'): void {
usePostListStore.getState().clearPostsState(key);
this.paginationManager.reset(key);
}
clearPostDetailState(postId: string): void {
usePostListStore.getState().clearPostDetailState(postId);
}
clearAllStates(): void {
usePostListStore.getState().clearAllStates();
this.paginationManager.clear();
}
getCurrentUserId(): string | null {
return this.currentUserId;
}
setCurrentUserId(userId: string | null): void {
this.currentUserId = userId;
}
getPaginationState(key: string): PaginationState {
return this.paginationManager.getState(key);
}
resetPagination(key: string): void {
this.paginationManager.reset(key);
}
isLoading(key: string = 'default'): boolean {
return usePostListStore.getState().getPostsState(key).isLoading;
}
hasError(key: string = 'default'): boolean {
return usePostListStore.getState().getPostsState(key).error !== null;
}
getError(key: string = 'default'): string | null {
return usePostListStore.getState().getPostsState(key).error;
}
}
export const postSyncService = PostSyncService.getInstance();
export { PostSyncService };
export default postSyncService;