refactor: migrate post operations to use case architecture with differential updates
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 3m11s
Frontend CI / ota-android (push) Failing after 16m3s
Frontend CI / build-android-apk (push) Has been cancelled

Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
This commit is contained in:
lafay
2026-03-21 20:55:36 +08:00
parent 25071d2303
commit d273569911
30 changed files with 4395 additions and 572 deletions

View File

@@ -0,0 +1,635 @@
/**
* 帖子差异更新 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<T extends PostIdentifier> {
/** 差异计算配置 */
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<T extends PostIdentifier> {
/** 优化后的帖子列表 */
posts: T[];
/** 是否正在加载 */
loading: boolean;
/** 是否正在刷新 */
refreshing: boolean;
/** 错误信息 */
error: string | null;
/** 是否有更多数据 */
hasMore: boolean;
/** 待处理更新数量 */
pendingUpdateCount: number;
/** 是否正在处理更新 */
isProcessing: boolean;
/** 刷新方法 */
refresh: () => Promise<void>;
/** 加载更多方法 */
loadMore: () => Promise<void>;
/** 手动刷新批量处理队列 */
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<T extends PostIdentifier = Post>(
initialPosts: T[] = [],
options: UseDifferentialPostsOptions<T> = {}
): UseDifferentialPostsResult<T> {
const {
diffConfig,
batcherOptions,
enableDiff = true,
enableBatching = true,
maxPostCount = 10000,
changeRatioThreshold = 0.5,
listKey = 'default',
autoSubscribe = true,
} = options;
// ==================== 状态 ====================
const [posts, setPosts] = useState<T[]>(initialPosts);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [hasMore, setHasMore] = useState(true);
const [isProcessing, setIsProcessing] = useState(false);
const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
const [diffUpdates, setDiffUpdates] = useState<DiffUpdatesInfo>({
addedCount: 0,
updatedCount: 0,
deletedCount: 0,
lastUpdateTime: 0,
});
// ==================== Refs ====================
const calculatorRef = useRef<PostDiffCalculator<T> | null>(null);
const batcherRef = useRef<PostUpdateBatcher | null>(null);
const previousPostsRef = useRef<T[]>(initialPosts);
const unsubscribeRef = useRef<(() => void) | null>(null);
// ==================== 初始化 ====================
// 初始化差异计算器
useEffect(() => {
if (enableDiff) {
calculatorRef.current = createPostDiffCalculator<T>(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;