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 { postSyncService } from '../services/post/PostSyncService'; import type { PostsState } from '../services/post/PostSyncService'; import { usePostListStore } from '../stores/post/postListStore'; import type { Post } from '../core/entities/Post'; export interface UseDifferentialPostsOptions { diffConfig?: PostDiffConfig; batcherOptions?: PostBatcherOptions; enableDiff?: boolean; enableBatching?: boolean; maxPostCount?: number; listKey?: string; autoSubscribe?: boolean; } export interface DiffUpdatesInfo { addedCount: number; updatedCount: number; deletedCount: number; lastUpdateTime: number; } 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; } 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, }); const calculatorRef = useRef | null>(null); const batcherRef = useRef(null); const previousPostsRef = useRef(initialPosts); 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(handleBatchUpdates); return () => { unsubscribe(); batcherRef.current?.destroy(); batcherRef.current = null; }; }, [enableBatching, batcherOptions, handleBatchUpdates]); const syncFromStore = useCallback((state: PostsState) => { setLoading(state.isLoading); setRefreshing(state.isRefreshing); setError(state.error); setHasMore(state.hasMore); 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 = []; } }, [maxPostCount]); useEffect(() => { if (!autoSubscribe) return; const currentState = usePostListStore.getState().getPostsState(listKey); syncFromStore(currentState); const unsubscribe = usePostListStore.subscribe(state => { const postsState = state.postsStateMap.get(listKey); if (postsState) syncFromStore(postsState); }); return unsubscribe; }, [autoSubscribe, listKey, syncFromStore]); const refresh = useCallback(async () => { setRefreshing(true); setError(null); try { await postSyncService.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 postSyncService.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;