/** * 帖子差异更新 Hook * 接收原始帖子列表,返回优化后的帖子列表,自动处理批量更新 * 集成 ProcessPostUseCase 进行状态管理 */ import { useState, useRef, useCallback, useEffect } from 'react'; import { PostIdentifier, PostUpdate, PostUpdateType, PostDiffConfig, } from '../infrastructure/diff/postTypes'; import { PostDiffCalculator, createPostDiffCalculator, } from '../infrastructure/diff/PostDiffCalculator'; import { PostUpdateBatcher, PostBatcherOptions, createPostUpdateBatcher, } from '../infrastructure/diff/PostUpdateBatcher'; import { processPostUseCase } from '../core/usecases/ProcessPostUseCase'; import type { PostUseCaseEvent, PostsState } from '../core/usecases/ProcessPostUseCase'; import type { Post } from '../core/entities/Post'; // ==================== 类型定义 ==================== /** * 差异更新 Hook 配置选项 */ export interface UseDifferentialPostsOptions { /** 差异计算配置 */ diffConfig?: PostDiffConfig; /** 批量处理配置 */ batcherOptions?: PostBatcherOptions; /** 是否启用差异计算,默认 true */ enableDiff?: boolean; /** 是否启用批量处理,默认 true */ enableBatching?: boolean; /** 最大帖子数量限制,超出时触发重置 */ maxPostCount?: number; /** 变化比例阈值(0-1),超过此值触发重置 */ changeRatioThreshold?: number; /** 列表键(用于区分不同的帖子列表) */ listKey?: string; /** 是否自动订阅 UseCase 状态变化,默认 true */ autoSubscribe?: boolean; } /** * 差异更新信息 */ export interface DiffUpdatesInfo { /** 新增数量 */ addedCount: number; /** 更新数量 */ updatedCount: number; /** 删除数量 */ deletedCount: number; /** 上次更新时间 */ lastUpdateTime: number; } /** * 差异更新 Hook 返回值 */ export interface UseDifferentialPostsResult { /** 优化后的帖子列表 */ posts: T[]; /** 是否正在加载 */ loading: boolean; /** 是否首屏加载 */ isInitialLoading: boolean; /** 是否正在加载更多 */ isLoadingMore: boolean; /** 是否正在刷新 */ refreshing: boolean; /** 错误信息 */ error: string | null; /** 是否有更多数据 */ hasMore: boolean; /** 待处理更新数量 */ pendingUpdateCount: number; /** 是否正在处理更新 */ isProcessing: boolean; /** 刷新方法 */ refresh: () => Promise; /** 加载更多方法 */ loadMore: () => Promise; /** 手动刷新批量处理队列 */ flush: () => void; /** 重置方法 */ reset: () => void; /** 强制更新(跳过批量处理) */ forceUpdate: (posts: T[]) => void; /** 获取差异统计信息 */ getDiffStats: () => { totalBatches: number; totalUpdates: number; averageBatchSize: number; }; /** 差异更新信息 */ diffUpdates: DiffUpdatesInfo; } // ==================== Hook 实现 ==================== /** * 帖子差异更新 Hook * @param initialPosts 初始帖子列表(可选) * @param options 配置选项 * @returns 优化后的帖子列表和相关方法 */ export function useDifferentialPosts( initialPosts: T[] = [], options: UseDifferentialPostsOptions = {} ): UseDifferentialPostsResult { const { diffConfig, batcherOptions, enableDiff = true, enableBatching = true, maxPostCount = 10000, listKey = 'default', autoSubscribe = true, } = options; // ==================== 状态 ==================== const [posts, setPosts] = useState(initialPosts); const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(null); const [hasMore, setHasMore] = useState(true); const [isProcessing, setIsProcessing] = useState(false); const [pendingUpdateCount, setPendingUpdateCount] = useState(0); const [diffUpdates, setDiffUpdates] = useState({ addedCount: 0, updatedCount: 0, deletedCount: 0, lastUpdateTime: 0, }); // ==================== Refs ==================== const calculatorRef = useRef | null>(null); const batcherRef = useRef(null); const previousPostsRef = useRef(initialPosts); const unsubscribeRef = useRef<(() => void) | null>(null); // ==================== 批量更新处理(须在订阅 batcher 的 useEffect 之前定义)==================== /** * 处理批量更新 */ const handleBatchUpdates = useCallback((updates: PostUpdate[]) => { setIsProcessing(true); try { setPosts((currentPosts) => { let newPosts = [...currentPosts]; let addedCount = 0; let updatedCount = 0; let deletedCount = 0; for (const update of updates) { switch (update.type) { case PostUpdateType.ADD: { const addUpdate = update as any; const index = addUpdate.index ?? newPosts.length; newPosts.splice(index, 0, addUpdate.post as T); addedCount++; break; } case PostUpdateType.BATCH_ADD: { const batchAddUpdate = update as any; const postsToAdd = batchAddUpdate.posts || []; const startIndex = batchAddUpdate.startIndex ?? newPosts.length; newPosts.splice(startIndex, 0, ...postsToAdd as T[]); addedCount += postsToAdd.length; break; } case PostUpdateType.UPDATE: { const updatePost = update as any; const index = newPosts.findIndex(p => p.id === updatePost.postId); if (index !== -1) { newPosts[index] = { ...newPosts[index], ...updatePost.updates }; updatedCount++; } break; } case PostUpdateType.BATCH_UPDATE: { const batchUpdate = update as any; const updatesList = batchUpdate.updates || []; for (const { postId, changes } of updatesList) { const index = newPosts.findIndex(p => p.id === postId); if (index !== -1) { newPosts[index] = { ...newPosts[index], ...changes }; updatedCount++; } } break; } case PostUpdateType.DELETE: { const deleteUpdate = update as any; const prevLength = newPosts.length; newPosts = newPosts.filter(p => p.id !== deleteUpdate.postId); if (newPosts.length < prevLength) { deletedCount++; } break; } case PostUpdateType.BATCH_DELETE: { const batchDelete = update as any; const idsToDelete = new Set(batchDelete.postIds || []); const prevLength = newPosts.length; newPosts = newPosts.filter(p => !idsToDelete.has(p.id)); deletedCount += prevLength - newPosts.length; break; } case PostUpdateType.MOVE: { const moveUpdate = update as any; const fromIndex = newPosts.findIndex(p => p.id === moveUpdate.postId); if (fromIndex !== -1) { const [movedPost] = newPosts.splice(fromIndex, 1); newPosts.splice(moveUpdate.toIndex, 0, movedPost); } break; } case PostUpdateType.RESET: { const resetUpdate = update as any; newPosts = resetUpdate.posts || []; break; } } } // 更新差异统计 if (addedCount > 0 || updatedCount > 0 || deletedCount > 0) { setDiffUpdates(prev => ({ addedCount: prev.addedCount + addedCount, updatedCount: prev.updatedCount + updatedCount, deletedCount: prev.deletedCount + deletedCount, lastUpdateTime: Date.now(), })); } return newPosts; }); } finally { setIsProcessing(false); setPendingUpdateCount(batcherRef.current?.getPendingCount() ?? 0); } }, []); // ==================== 初始化 ==================== // 初始化差异计算器 useEffect(() => { if (enableDiff) { calculatorRef.current = createPostDiffCalculator(diffConfig); } return () => { calculatorRef.current = null; }; }, [enableDiff, diffConfig]); // 初始化批量处理器 useEffect(() => { if (!enableBatching) return; batcherRef.current = createPostUpdateBatcher(batcherOptions); const unsubscribe = batcherRef.current.subscribe((updates) => { handleBatchUpdates(updates); }); return () => { unsubscribe(); batcherRef.current?.destroy(); batcherRef.current = null; }; }, [enableBatching, batcherOptions, handleBatchUpdates]); // ==================== UseCase 订阅 ==================== /** * 处理 UseCase 事件 */ const handleUseCaseEvent = useCallback((event: PostUseCaseEvent) => { switch (event.type) { case 'state_changed': { const { key: eventKey, state } = event.payload as { key: string; state: PostsState }; // 全局订阅:只应用当前列表 key,避免其他 Tab/列表的 state 覆盖本列表(如 createPost 会更新所有 key) if (eventKey !== listKey) { break; } if (state) { setLoading(state.isLoading); setRefreshing(state.isRefreshing); setError(state.error); setHasMore(state.hasMore); // ProcessPostUseCase 的 state.posts 已是合并后的完整列表。 // 不再走差异计算 + PostUpdateBatcher:批量应用晚于 previousPostsRef 更新时, // BATCH_ADD 会在过短的 currentPosts 上 splice,导致连续游标加载「请求成功但列表不增长」。 batcherRef.current?.clearPending(); calculatorRef.current?.reset(); if (state.posts && state.posts.length > 0) { const next = state.posts as unknown as T[]; if (next.length > maxPostCount) { console.warn(`[useDifferentialPosts] Post count (${next.length}) exceeds limit (${maxPostCount})`); } setPosts(next); previousPostsRef.current = next; } else { setPosts([]); previousPostsRef.current = []; } } break; } case 'posts_loaded': { // fetchPosts 在 updatePostsState 时已发出 state_changed(含合并后 posts),此处不再应用,避免与其它 key 竞态或重复计算 break; } case 'post_created': { const { post } = event.payload; if (post) { // 新帖子添加到列表开头 setPosts(prev => [post as T, ...prev]); setDiffUpdates(prev => ({ ...prev, addedCount: prev.addedCount + 1, lastUpdateTime: Date.now(), })); } break; } case 'post_updated': { const { post } = event.payload; if (post) { setPosts(prev => prev.map(p => p.id === post.id ? { ...p, ...post } as T : p)); setDiffUpdates(prev => ({ ...prev, updatedCount: prev.updatedCount + 1, lastUpdateTime: Date.now(), })); } break; } case 'post_deleted': { const { postId } = event.payload; if (postId) { setPosts(prev => prev.filter(p => p.id !== postId)); setDiffUpdates(prev => ({ ...prev, deletedCount: prev.deletedCount + 1, lastUpdateTime: Date.now(), })); } break; } case 'loading_changed': { const { isLoading } = event.payload; setLoading(isLoading); break; } case 'error_occurred': { const payload = event.payload as { key?: string; error?: string }; if (payload.key !== undefined && payload.key !== listKey) { break; } if (payload.error !== undefined) { setError(payload.error); } break; } } }, [listKey, maxPostCount]); // 订阅 UseCase 状态变化 useEffect(() => { if (autoSubscribe) { unsubscribeRef.current = processPostUseCase.subscribe(handleUseCaseEvent); // 初始化时获取当前状态 const currentState = processPostUseCase.getPostsState(listKey); if (currentState.posts.length > 0) { setPosts(currentState.posts as unknown as T[]); previousPostsRef.current = currentState.posts as unknown as T[]; } setLoading(currentState.isLoading); setRefreshing(currentState.isRefreshing); setError(currentState.error); setHasMore(currentState.hasMore); return () => { if (unsubscribeRef.current) { unsubscribeRef.current(); unsubscribeRef.current = null; } }; } }, [autoSubscribe, listKey, handleUseCaseEvent]); // ==================== 操作方法 ==================== /** * 刷新帖子列表 */ const refresh = useCallback(async () => { setRefreshing(true); setError(null); try { await processPostUseCase.refreshPosts(listKey); } catch (err) { const errorMsg = err instanceof Error ? err.message : '刷新失败'; setError(errorMsg); } finally { setRefreshing(false); } }, [listKey]); /** * 加载更多帖子 */ const loadMore = useCallback(async () => { if (hasMore && !loading) { setLoading(true); setError(null); try { await processPostUseCase.loadMorePosts(listKey); } catch (err) { const errorMsg = err instanceof Error ? err.message : '加载更多失败'; setError(errorMsg); } finally { setLoading(false); } } }, [listKey, hasMore, loading]); /** * 手动刷新批量处理队列 */ const flush = useCallback(() => { batcherRef.current?.flush(); }, []); /** * 重置方法 */ const reset = useCallback(() => { calculatorRef.current?.reset(); batcherRef.current?.clearPending(); setPosts([]); setLoading(false); setRefreshing(false); setError(null); setHasMore(true); previousPostsRef.current = []; setDiffUpdates({ addedCount: 0, updatedCount: 0, deletedCount: 0, lastUpdateTime: 0, }); }, []); /** * 强制更新方法 */ const forceUpdate = useCallback((newPosts: T[]) => { setPosts(newPosts); previousPostsRef.current = newPosts; }, []); /** * 获取统计信息 */ const getDiffStats = useCallback(() => { const batcherStats = batcherRef.current?.getStats(); return { totalBatches: batcherStats?.totalBatches ?? 0, totalUpdates: batcherStats?.totalUpdates ?? 0, averageBatchSize: batcherStats?.averageBatchSize ?? 0, }; }, []); // ==================== 定期更新待处理数量 ==================== useEffect(() => { if (!enableBatching || !batcherRef.current) return; const interval = setInterval(() => { setPendingUpdateCount(batcherRef.current?.getPendingCount() ?? 0); }, 50); return () => clearInterval(interval); }, [enableBatching]); // ==================== 返回值 ==================== return { posts, loading, isInitialLoading: loading && posts.length === 0 && !refreshing, isLoadingMore: loading && posts.length > 0 && !refreshing, refreshing, error, hasMore, pendingUpdateCount, isProcessing, refresh, loadMore, flush, reset, forceUpdate, getDiffStats, diffUpdates, }; } export default useDifferentialPosts;