/** * 帖子差异更新 Hook * 接收原始帖子列表,返回优化后的帖子列表,自动处理批量更新 * 集成 ProcessPostUseCase 进行状态管理 */ import { useState, useRef, useCallback, useEffect, useMemo } from 'react'; import { PostIdentifier, PostUpdate, PostUpdateType, PostDiffConfig, PostDiffResult, } 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; /** 是否正在刷新 */ 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, changeRatioThreshold = 0.5, 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); // ==================== 初始化 ==================== // 初始化差异计算器 useEffect(() => { if (enableDiff) { calculatorRef.current = createPostDiffCalculator(diffConfig); } return () => { calculatorRef.current = null; }; }, [enableDiff, diffConfig]); // 初始化批量处理器 useEffect(() => { if (enableBatching) { batcherRef.current = createPostUpdateBatcher(batcherOptions); // 订阅批量更新 const unsubscribe = batcherRef.current.subscribe((updates) => { handleBatchUpdates(updates); }); return () => { unsubscribe(); batcherRef.current?.destroy(); batcherRef.current = null; }; } }, [enableBatching, batcherOptions]); // ==================== 批量更新处理 ==================== /** * 处理批量更新 */ 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); } }, []); // ==================== 差异计算 ==================== /** * 计算并应用差异 */ const calculateAndApplyDiff = useCallback((newPosts: T[]) => { const previousPosts = previousPostsRef.current; // 如果帖子数量变化太大,直接重置 const previousCount = previousPosts.length; const currentCount = newPosts.length; const changeRatio = previousCount === 0 ? 1 : Math.abs(currentCount - previousCount) / previousCount; if (changeRatio > changeRatioThreshold) { setPosts(newPosts); previousPostsRef.current = newPosts; return; } // 使用差异计算 if (enableDiff && calculatorRef.current) { const diff = calculatorRef.current.calculateDiff(previousPosts, newPosts); if (diff.shouldReset) { setPosts(newPosts); } else { // 将差异转换为更新操作 const updates: PostUpdate[] = []; // 处理新增 if (diff.added.length > 0) { if (diff.added.length === 1) { updates.push({ type: PostUpdateType.ADD, post: diff.added[0], timestamp: Date.now(), }); } else { updates.push({ type: PostUpdateType.BATCH_ADD, posts: diff.added, timestamp: Date.now(), }); } } // 处理更新 if (diff.updated.length > 0) { if (diff.updated.length === 1) { updates.push({ type: PostUpdateType.UPDATE, postId: diff.updated[0].post.id, updates: diff.updated[0].changes, timestamp: Date.now(), }); } else { updates.push({ type: PostUpdateType.BATCH_UPDATE, updates: diff.updated.map(u => ({ postId: u.post.id, changes: u.changes, })), timestamp: Date.now(), }); } } // 处理删除 if (diff.deleted.length > 0) { if (diff.deleted.length === 1) { updates.push({ type: PostUpdateType.DELETE, postId: diff.deleted[0].post.id, timestamp: Date.now(), }); } else { updates.push({ type: PostUpdateType.BATCH_DELETE, postIds: diff.deleted.map(d => d.post.id), timestamp: Date.now(), }); } } // 处理移动 if (diff.moved.length > 0) { for (const move of diff.moved) { updates.push({ type: PostUpdateType.MOVE, postId: move.post.id, toIndex: move.toIndex, timestamp: Date.now(), }); } } // 应用更新 if (updates.length > 0) { if (enableBatching && batcherRef.current) { batcherRef.current.addUpdates(updates); } else { handleBatchUpdates(updates); } } } } else { // 不使用差异计算,直接设置 setPosts(newPosts); } // 更新帖子数量限制检查 if (newPosts.length > maxPostCount) { console.warn(`[useDifferentialPosts] Post count (${newPosts.length}) exceeds limit (${maxPostCount})`); } previousPostsRef.current = newPosts; }, [enableDiff, enableBatching, maxPostCount, changeRatioThreshold, handleBatchUpdates]); // ==================== UseCase 订阅 ==================== /** * 处理 UseCase 事件 */ const handleUseCaseEvent = useCallback((event: PostUseCaseEvent) => { switch (event.type) { case 'state_changed': { const { state } = event.payload as { key: string; state: PostsState }; if (state) { setLoading(state.isLoading); setRefreshing(state.isRefreshing); setError(state.error); setHasMore(state.hasMore); // 如果帖子列表有变化,进行差异计算 if (state.posts && state.posts.length > 0) { calculateAndApplyDiff(state.posts as unknown as T[]); } } break; } case 'posts_loaded': { const { posts: loadedPosts } = event.payload; if (loadedPosts) { calculateAndApplyDiff(loadedPosts as T[]); } 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 { error: errorMsg } = event.payload; setError(errorMsg); break; } } }, [calculateAndApplyDiff]); // 订阅 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, refreshing, error, hasMore, pendingUpdateCount, isProcessing, refresh, loadMore, flush, reset, forceUpdate, getDiffStats, diffUpdates, }; } export default useDifferentialPosts;